Search code examples
javascripthtmlreactjscanvas

Draggable canvas image getting stuck at the border when getting dragged outside the canvas


I'm using React, I have a canvas with a square inside that can be dragged around. When the square touches the borders I want it to be reset back to its starting position. This behavior is working fine when everything is slow and smooth, but if I drag the mouse fast the image gets stuck. By stuck I mean the image either becomes unresponsive leading to a page reload, or it snaps back to its original position on returning the mouse back to the canvas.

enter image description here

const Canvas = props => {   
        

        useEffect(() =>{
            
            const canvas = canvasRef.current
            const context = canvas.getContext('2d')
            

            canvas.width = canvas.getBoundingClientRect().width
            canvas.height = canvas.getBoundingClientRect().height

            setCanvasWidth(canvas.width);
            setCanvasHeight(canvas.height);


            canvas.addEventListener('mousedown', e => {
                
                var rect = e.target.getBoundingClientRect();
                var x = e.clientX - rect.left; //x position within the element.
                var y = e.clientY - rect.top;  //y position within the element.

                if(x>playerX && (x<playerX+playerWidth) && y>playerY && (y<playerY+playerHeight) && (gameStatus==false)){
                    
                    setGameStatus(true);
                    setPlayerStatus(true);

                    

                else if(x>playerX && (x<playerX+playerWidth) && y>playerY && (y<playerY+playerHeight) && (gameStatus==true)){
                    setPlayerStatus(true);
                }
                
            });


            canvas.addEventListener('mousemove', e => {
                
                var rect = e.target.getBoundingClientRect();
                var x = e.clientX - rect.left; //x position within the element.
                var y = e.clientY - rect.top;  //y position within the element.

                
                //if you are currently controlling the player, x and y coordinates are set to mouse

                if(playerStatus){
                    setPlayerX(x - playerWidth/2);
                    setPlayerY(y - playerHeight/2);                         
                }

                //if the box edges touch the borders
                if(
                    
                    ((playerX <= 0 || playerX >= canvasWidth-playerWidth || playerY <= 0 || playerY >= canvasHeight-playerHeight)&&gameStatus)
                
                ){

                    console.log('playerX: '+playerX)
                    console.log('playerY: '+playerY)
                    console.log('CanvasWidthLeft: '+ canvasWidth*(0.0001))
                    console.log('CanvasWidthRight: '+ canvasWidth)
                    console.log('CanvasHeightUp: '+ canvasHeight*(0.0001))
                    console.log('CanvasHeightDown: '+ canvasHeight)

                    setPlayerX(canvasWidth*(0.5) - playerWidth/2)
                    setPlayerY(canvasHeight*(0.5) - playerHeight/2)
                    setPlayerStatus(false);
                    setGameStatus(false);

                    



            });


            canvas.addEventListener('mouseup', () => {
                setPlayerStatus(false);

                

            })

context.fillStyle = '#00f4cc'
context.fillRect(playerX, playerY, playerWidth, playerHeight) 

The way the canvas is working is by having the 3 event listeners from above. The square begins in the middle of the canvas, when clicking it the playerStatus and gameStatus states change to indicate square is draggable. If the edges of the square touch the border it will snap back to starting. Everything works find when things are slow, but if things are fast it glitches out. Does anyone know a solution to 'immediately' stop the square from moving and to snap back to place if it touches the borders? Thanks!

EDIT:

Here are some more solutions I tried but didnt work out:


canvas.addEventListener('mouseleave', (e) => {

                var rect = e.target.getBoundingClientRect();
                var x = e.clientX - rect.left; //x position within the element.
                var y = e.clientY - rect.top;  //y position within the element.              

                if(gameStatus){                  
                    canvas.draggable = false;
                }


            })


canvas.addEventListener('mouseout', (e) => {

                var rect = e.target.getBoundingClientRect();
                var x = e.clientX - rect.left; //x position within the element.
                var y = e.clientY - rect.top;  //y position within the element.              

                if(gameStatus){                  
                    setPlayerX( 100 )
                    setPlayerY( 100 )
                }


            })



Solution

  • You can use mouseleave to detect when the mouse leaves the canvas and reset the position there, everything else seem to be OK, you might have some other performance issues on the functions you are not showing...

    Below is small sample, I have the same 3 event listeners, plus the mouseleave

    const canvas = document.getElementById("canvas")
    const ctx = canvas.getContext("2d")
    var player = { x: canvas.width / 2, y: canvas.height / 2, draggable: false }
    
    function draw() {
      ctx.beginPath()
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.arc(player.x, player.y, 10, 0, 2 * Math.PI);
      ctx.fill()
      requestAnimationFrame(draw);
    }
    draw()
    
    canvas.addEventListener('mouseleave', e => {
      player = { x: canvas.width / 2, y: canvas.height / 2, draggable: false }
    })
    
    canvas.addEventListener('mousedown', e => {
      var rect = e.target.getBoundingClientRect();
      var x = e.clientX - rect.left; 
      var y = e.clientY - rect.top;
      player.draggable = Math.hypot(x - player.x, y - player.y) < 20
    });
    
    canvas.addEventListener('mousemove', e => {
      if (player.draggable) {
        var rect = e.target.getBoundingClientRect();
        player.x = e.clientX - rect.left;
        player.y = e.clientY - rect.top;
      }
    });
    
    canvas.addEventListener('mouseup', () => {
      player.draggable = false
    })
    canvas { border :solid }
    <canvas id="canvas"> </canvas>


    Here is another with a square:

    const canvas = document.getElementById("canvas")
    const ctx = canvas.getContext("2d")
    var player = { x: canvas.width / 2, y: canvas.height / 2, draggable: false }
    
    function draw() {
      ctx.beginPath()
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillRect(player.x -10, player.y -10, 20, 20);
      requestAnimationFrame(draw);
    }
    draw()
    
    canvas.addEventListener('mouseleave', e => {
      player = { x: canvas.width / 2, y: canvas.height / 2, draggable: false }
    })
    
    canvas.addEventListener('mousedown', e => {
      var rect = e.target.getBoundingClientRect();
      var x = e.clientX - rect.left; 
      var y = e.clientY - rect.top;
      player.draggable = x > player.x -10 && x < player.x +10 && y > player.y -10 && y < player.y +10
    });
    
    canvas.addEventListener('mousemove', e => {
      if (player.draggable) {
        var rect = e.target.getBoundingClientRect();
        player.x = e.clientX - rect.left;
        player.y = e.clientY - rect.top;
      }
    });
    
    canvas.addEventListener('mouseup', () => {
      player.draggable = false
    })
    canvas { border :solid }
    <canvas id="canvas"> </canvas>

    All that is need is to change the condition when is the player draggable