marci sz marci sz - 6 months ago 11
Javascript Question

Detecting clicks versus drags on an html 5 canvas

I am trying to write a simple map with the HTML 5 canvas. Dragging the mouse moves the map, and clicking selects the closest waypoint. However, there is a problem. When I drag the mouse, I still get the click event when I release it. I would like to get that only if there was no movement while clicking.

I tried to check for a no movement event in the mousemove event handler, but with no success:

function onMove(e) {
if(e.movementX == 0 && e.movementY == 0) {
console.log("a"); //never happens...
}
}


My question is: is there an easy way to do it, or should I check if the press and release event were at the same place?

Here is my code:

function onMove(e) {
console.log("moving");
}

function onClick(e) {
console.log("clicked");
}

function init() {
c = document.getElementById("MapCanvas");
ctx = c.getContext("2d");
c.addEventListener("click", onClick, true);
c.addEventListener("mousemove", onMove, true);
}

K3N K3N
Answer

Here is one way to solve this:

As we need to know two states we need two flags (some prefer objects - it's up to you):

var isDown = false;    // mouse button is held down
var isMoving = false;  // we're moving (dragging)

Then we need to find a way to distinguish between a drag and a click. A typical approach is to use a radius (length) between first click point to the current point: if outside we consider it to be a drag operation.

So -

var radius = 9 * 9     // radius in pixels, 9 squared
var firstPos;          // keep track of first position

(We're squaring the 9 so we don't have to calculate square-root for every mouse move event later).

Then we can define our callback handlers to consider these things:

canvas.onmousedown = function(e) {
  firstPos = getXY(e); // record click position (see getXY function in demo)
  isDown = true;       // record mouse state
  isMoving = false;    // reset move state
};

The next handlers can be set on window object instead of the canvas element itself. This allows us to move outside the canvas element while the mouse button is held. We need to use addEventListener() here so we allow other code to subscribe as well:

window.addEventListener("mousemove", function(e) {
  if (!isDown) return; // we will only act if mouse button is down
  var pos = getXY(e);  // get current mouse position

  // calculate distance from click point to current point
  var dx = firstPos.x - pos.x,
      dy = firstPos.y - pos.y,
      dist = dx * dx + dy * dy;        // skip square-root (see above)

  if (dist >= radius) isMoving = true; // 10-4 we're on the move

  if (isMoving) {
    // handle move operation here
  }
});

And finally we detect for click as well as updating mouse state, also this on the window object:

window.addEventListener("mouseup", function(e) {
  if (!isDown) return;   // no need for us in this case
  isDown = false;        // record mouse state

  if (!isMoving) {
    // it was a click, handle click operation here
  }
});

Then final problem is then to click the way-point. Doing an absolute check (i.e. x === value) will rarely turn out OK as we need to place the mouse button exactly at that point. Allow a range using the width and height of the way point (assuming an object wp for the way-point):

if (pos.x >= wp.x && pos.x < wp.x + wp.width && 
    pos.y >= wp.y && pos.y < wp.y + wp.height) { ... }

Example

var ctx = canvas.getContext("2d");
var wp = {x: 50, y:50, width:12, height:12};  // demo way-point
ctx.font = "20px sans-serif";
ctx.fillText("Click or click+move on this canvas...", 10, 30);
ctx.strokeRect(wp.x, wp.y, wp.width, wp.height);

var isDown = false;        // mouse button is held down
var isMoving = false;      // we're moving (dragging)
var radius = 9 * 9         // radius in pixels, 9 squared
var firstPos;              // keep track of first position

canvas.onmousedown = function(e) {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.strokeRect(wp.x, wp.y, wp.width, wp.height);
  
  firstPos = getXY(e);
  isDown = true;           // record mouse state
  isMoving = false;        // reset move state
};

window.addEventListener("mousemove", function(e) {
  if (!isDown) return;     // we will only act if mouse button is down
  var pos = getXY(e);      // get current mouse position

  // calculate distance from click point to current point
  var dx = firstPos.x - pos.x,
      dy = firstPos.y - pos.y,
      dist = dx * dx + dy * dy;        // skip square-root (see above)

  if (dist >= radius) isMoving = true; // 10-4 we're on the move

  if (isMoving) {
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.strokeRect(wp.x, wp.y, wp.width, wp.height);
    ctx.fillText("MOVING", 10, 30);
  }
});

window.addEventListener("mouseup", function(e) {
  if (!isDown) return;     // no need for us in this case
  isDown = false;          // record mouse state

  if (!isMoving) {
    if (firstPos.x >= wp.x && firstPos.x < wp.x + wp.width &&
        firstPos.y >= wp.y && firstPos.y < wp.y + wp.height) {
      ctx.fillText("CLICKED WAYPOINT", 10, 30);
    }
    else {
      ctx.fillText("CLICK", 10, 30);
    }
  }
});

function getXY(e) {
  var rect = canvas.getBoundingClientRect();
  return {x: e.clientX - rect.left, y: e.clientY - rect.top}
}
canvas {background:#ccc}
<canvas id=canvas width=620 height=180></canvas>