Search code examples
javascripthtmlhtml5-canvas

HTML Canvas drag and drop + snap functionality


I made this simple example of a drag-and-drop circle inside an HTML canvas. Here's the code below:

var c = document.getElementById('myCanvas');
var ctx = c.getContext('2d');
width = c.width = window.innerWidth * 0.9;
height = c.height = window.innerHeight * 0.9;

var handle = {
    x: width / 2,
    y: height / 2,
    radius: 30,
};

function draw() {
    ctx.clearRect(0, 0, width, height);
    ctx.beginPath();
    ctx.arc(handle.x, handle.y, handle.radius, 0, Math.PI * 2, false);
    ctx.fill();
    ctx.stroke();
    drawLines();
}

function drawLines() {
    ctx.beginPath();
    ctx.moveTo(0, height / 2);
    ctx.lineTo(width, height / 2);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(width / 6, 0);
    ctx.lineTo(width / 6, height);
    ctx.stroke();
}

function circlePointCollision(x, y, circle) {
    return distanceXY(x, y, circle.x, circle.y) < circle.radius;
}

function distanceXY(x0, y0, x1, y1) {
    var dx = x1 - x0,
        dy = y1 - y0;
    return Math.sqrt(dx * dx + dy * dy);
}

document.addEventListener('mousedown', function (e) {
    if (circlePointCollision(e.x , e.y , handle)) {
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
    }
});

function onMouseMove(e) {
    handle.x = e.pageX;
    handle.y = e.pageY;
    draw();
}

function onMouseUp() {
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
}
draw();
drawLines();
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <canvas id="myCanvas" style="border: 1px solid black"></canvas>
    <script src="./script.js"></script>
  </body>
</html>

I created this with a help of some resources from this video and these are the materials they used. I'm trying to write a proof of concept for snap functionality. Basically, I want to make that circle "snap" in the middle of the crossing two lines when it's being moved across (or very close to). When it's snapped in the intersection, it should be a little bit harder to move it from there. Due to being relatively new to HTML canvas, I'm not exactly sure what the best approach is.

Can anyone help me to expand my snippet and make it do what I need?


Solution

  • Before assigning handle.x and handle.y in onMouseMove simply compare e.pageX and e.pageY to the intersection point and if they are close enough set the the handle coordinates to the intersection instead of e.pageX and e.pageY.

    I've created local variables here as an example, but of course you would want to either have a snap function that maintained the threshold and a list of intersections (or a grid declaration of some sort) to check against or declare threshold and intersection globally and check against them.

    function onMouseMove(e) {
      const threshold = handle.radius * 0.8;
      const intersection = { x: width / 6, y: height / 2 }
    
      handle.x = distanceXY(e.pageX, e.pageY, intersection.x, intersection.y) < threshold ? intersection.x : e.pageX;
      handle.y = distanceXY(e.pageX, e.pageY, intersection.x, intersection.y) < threshold ? intersection.y : e.pageY;
      draw();
    }
    

    var c = document.getElementById('myCanvas');
    var ctx = c.getContext('2d');
    width = c.width = window.innerWidth * 0.9;
    height = c.height = window.innerHeight * 0.9;
    
    
    var handle = {
      x: width / 2,
      y: height / 2,
      radius: 30,
    };
    
    function draw() {
      ctx.clearRect(0, 0, width, height);
      ctx.beginPath();
      ctx.arc(handle.x, handle.y, handle.radius, 0, Math.PI * 2, false);
      ctx.fill();
      ctx.stroke();
      drawLines();
    }
    
    function drawLines() {
      ctx.beginPath();
      ctx.moveTo(0, height / 2);
      ctx.lineTo(width, height / 2);
      ctx.stroke();
    
      ctx.beginPath();
      ctx.moveTo(width / 6, 0);
      ctx.lineTo(width / 6, height);
      ctx.stroke();
    }
    
    function circlePointCollision(x, y, circle) {
      return distanceXY(x, y, circle.x, circle.y) < circle.radius;
    }
    
    function distanceXY(x0, y0, x1, y1) {
      var dx = x1 - x0,
        dy = y1 - y0;
      return Math.sqrt(dx * dx + dy * dy);
    }
    
    document.addEventListener('mousedown', function (e) {
      if (circlePointCollision(e.x, e.y, handle)) {
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup', onMouseUp);
      }
    });
    
    function onMouseMove(e) {
      const threshold = handle.radius * 0.8;
      const intersection = { x: width / 6, y: height / 2 }
    
      handle.x = distanceXY(e.pageX, e.pageY, intersection.x, intersection.y) < threshold ? intersection.x : e.pageX;
      handle.y = distanceXY(e.pageX, e.pageY, intersection.x, intersection.y) < threshold ? intersection.y : e.pageY;
      draw();
    }
    
    function onMouseUp() {
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
    }
    draw();
    drawLines();
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <canvas id="myCanvas" style="border: 1px solid black"></canvas>
        <script src="./script.js"></script>
      </body>
    </html>