Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Most modern method of getting mouse position within a canvas in native JavaScript [duplicate]

First, I know this question has been asked many times. However, the answers provided are not consistent and a variety of methods are used to get the mouse position. A few examples:

Method 1:

canvas.onmousemove = function (event) { // this  object refers to canvas object  
    Mouse = {
        x: event.pageX - this.offsetLeft,
        y: event.pageY - this.offsetTop
    }
}

Method 2:

function getMousePos(canvas, evt) {
    var rect = canvas.getBoundingClientRect();
    return {
        x: evt.clientX - rect.left,
        y: evt.clientY - rect.top
    };
}

Method 3:

var findPos = function(obj) {
    var curleft = curtop = 0;
    if (obj.offsetParent) { 
        do {
            curleft += obj.offsetLeft;
            curtop += obj.offsetTop; 
        } while (obj = obj.offsetParent);
    }
    return { x : curleft, y : curtop };
};

Method 4:

var x;
var y;
if (e.pageX || e.pageY)
{
    x = e.pageX;
    y = e.pageY;
}
else {
    x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 
} 
x -= gCanvasElement.offsetLeft;
y -= gCanvasElement.offsetTop;

and so on.

What I am curious is which method is the most modern in terms of browser support and convenience in getting the mouse position in a canvas. Or is it those kind of things that have marginal impact and any of the above is a good choice? (Yes I realize the codes above are not exactly the same)

like image 552
No Harm In Trying Avatar asked Nov 19 '13 00:11

No Harm In Trying


People also ask

How do you get the mouse position on canvas?

The dimension of the canvas is found using the getBoundingClientRect() function. This method returns the size of an element and its position relative to the viewport. The position of x-coordinate of the mouse click is found by subtracting the event's x position with the bounding rectangle's x position.

How do I get current mouse position?

To get the current mouse position we are going to trigger a mouse event. In this case we will use 'mousemove' to log the current X and Y coordinates of the mouse to the console. For a more detailed list of mouse events you could have a read of this.

How do I find the cursor position in an element?

You can use the jQuery event. pageX and event. pageY in combination with the jQuery offset() method to get the position of mouse pointer relative to an element.


2 Answers

This seems to work. I think this is basically what K3N said.

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

function getStyleSize(style, propName) {
  return parseInt(style.getPropertyValue(propName));
}

// assumes target or event.target is canvas
function getCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);

  // you can remove this if padding is 0. 
  // I hope this always returns "px"
  var style = window.getComputedStyle(target);
  var nonContentWidthLeft   = getStyleSize(style, "padding-left") +
                              getStyleSize(style, "border-left");
  var nonContentWidthTop    = getStyleSize(style, "padding-top") +
                              getStyleSize(style, "border-top");
  var nonContentWidthRight  = getStyleSize(style, "padding-right") +
                              getStyleSize(style, "border-right");
  var nonContentWidthBottom = getStyleSize(style, "padding-bottom") +
                              getStyleSize(style, "border-bottom");

  var rect = target.getBoundingClientRect();
  var contentDisplayWidth  = rect.width  - nonContentWidthLeft - nonContentWidthRight;
  var contentDisplayHeight = rect.height - nonContentWidthTop  - nonContentWidthBottom;

  pos.x = (pos.x - nonContentWidthLeft) * target.width  / contentDisplayWidth;
  pos.y = (pos.y - nonContentWidthTop ) * target.height / contentDisplayHeight;

  return pos;  
}

If you run the sample below and move the mouse over the blue area it will draw under the cursor. The border (black), padding (red), width, and height are all set to non-pixel values values. The blue area is the actual canvas pixels. The canvas's resolution is not set so it's 300x150 regardless of the size it's stretched to.

Move the mouse over the blue area and it will draw a pixel under it.

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");

function clearCanvas() {
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
clearCanvas();

var posNode = document.createTextNode("");
document.querySelector("#position").appendChild(posNode);

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

function getStyleSize(style, propName) {
  return parseInt(style.getPropertyValue(propName));
}

// assumes target or event.target is canvas
function getCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);
  
  // you can remove this if padding is 0. 
  // I hope this always returns "px"
  var style = window.getComputedStyle(target);
  var nonContentWidthLeft   = getStyleSize(style, "padding-left") +
                              getStyleSize(style, "border-left");
  var nonContentWidthTop    = getStyleSize(style, "padding-top") +
                              getStyleSize(style, "border-top");
  var nonContentWidthRight  = getStyleSize(style, "padding-right") +
                              getStyleSize(style, "border-right");
  var nonContentWidthBottom = getStyleSize(style, "padding-bottom") +
                              getStyleSize(style, "border-bottom");
  
  var rect = target.getBoundingClientRect();
  var contentDisplayWidth  = rect.width  - nonContentWidthLeft - nonContentWidthRight;
  var contentDisplayHeight = rect.height - nonContentWidthTop  - nonContentWidthBottom;

  pos.x = (pos.x - nonContentWidthLeft) * target.width  / contentDisplayWidth;
  pos.y = (pos.y - nonContentWidthTop ) * target.height / contentDisplayHeight;
  
  return pos;  
}

  
function handleMouseEvent(event) {
  var pos = getCanvasRelativeMousePosition(event);
  posNode.nodeValue = JSON.stringify(pos, null, 2);
  ctx.fillStyle = "white";
  ctx.fillRect(pos.x | 0, pos.y | 0, 1, 1);
}

canvas.addEventListener('mousemove', handleMouseEvent);
canvas.addEventListener('click', clearCanvas);
* {
  box-sizing: border-box;
  cursor: crosshair;
}
html, body {
  width: 100%;
  height: 100%;
  color: white;
}
.outer {
  background-color: green;
  display: flex;
  display: -webkit-flex;
  
  -webkit-justify-content: center;
  -webkit-align-content: center;
  -webkit-align-items: center;

  justify-content: center;
  align-content: center;
  align-items: center;  
  
  width: 100%;
  height: 100%;
}
.inner {
  border: 1em solid black;
  background-color: red;
  padding: 1.5em;
  width: 90%;
  height: 90%;
}
#position {
  position: absolute;
  left: 1em;
  top: 1em;
  z-index: 2;
  pointer-events: none;
}
<div class="outer">
  <canvas class="inner"></canvas>
</div>
<pre id="position"></pre>

So, best advice?, always have the border and padding of a canvas be 0 if unless you want to go through all these steps. If the border and padding are zero you can just canvas.clientWidth and canvas.clientHeight for contentDisplayWidth and contentDisplayHeight in the example below and all the nonContextXXX values become 0.

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);

  pos.x = pos.x * target.width  / canvas.clientWidth;
  pos.y = pos.y * target.height / canvas.clientHeight;

  return pos;  
}

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");

function clearCanvas() {
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
clearCanvas();

var posNode = document.createTextNode("");
document.querySelector("#position").appendChild(posNode);

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);
  
  pos.x = pos.x * target.width  / canvas.clientWidth;
  pos.y = pos.y * target.height / canvas.clientHeight;
  
  return pos;  
}

  
function handleMouseEvent(event) {
  var pos = getNoPaddingNoBorderCanvasRelativeMousePosition(event);
  posNode.nodeValue = JSON.stringify(pos, null, 2);
  ctx.fillStyle = "white";
  ctx.fillRect(pos.x | 0, pos.y | 0, 1, 1);
}

canvas.addEventListener('mousemove', handleMouseEvent);
canvas.addEventListener('click', clearCanvas);
* {
  box-sizing: border-box;
  cursor: crosshair;
}
html, body {
  width: 100%;
  height: 100%;
  color: white;
}
.outer {
  background-color: green;
  display: flex;
  display: -webkit-flex;
  
  -webkit-justify-content: center;
  -webkit-align-content: center;
  -webkit-align-items: center;

  justify-content: center;
  align-content: center;
  align-items: center;  
  
  width: 100%;
  height: 100%;
}
.inner {
  background-color: red;
  width: 90%;
  height: 80%;
  display: block;
}
#position {
  position: absolute;
  left: 1em;
  top: 1em;
  z-index: 2;
  pointer-events: none;
}
<div class="outer">
  <canvas class="inner"></canvas>
</div>
<pre id="position"></pre>
like image 69
gman Avatar answered Sep 21 '22 08:09

gman


You target canvas, so you target only recent browsers.
So you can forget about the pageX stuff of Method 4.
Method 1 fails in case of nested canvas.
Method 3 is just like Method 2, but slower since you do it by hand.

-->> The way to go is option 2.

Now since you worry about performances, you don't want to call to the DOM on each mouse move : cache the boundingRect left and top inside some var/property.

If your page allows scrolling, do not forget to handle the 'scroll' event and to re-compute the bounding rect on scroll.

The coordinates are provided in css pixels : If you scale the Canvas with css, be sure its border is 0 and use offsetWidth and offsetHeight to compute correct position. Since you will want to cache also those values for performances and avoid too many globals, code will look like :

var mouse = { x:0, y:0, down:false };

function setupMouse() {

    var rect = cv.getBoundingClientRect();
    var rectLeft = rect.left;
    var rectTop = rect.top;

    var cssScaleX = cv.width / cv.offsetWidth;
    var cssScaleY = cv.height / cv.offsetHeight;

    function handleMouseEvent(e) {
        mouse.x = (e.clientX - rectLeft) * cssScaleX;
        mouse.y = (e.clientY - rectTop) * cssScaleY;
    }

    window.addEventListener('mousedown', function (e) {
        mouse.down = true;
        handleMouseEvent(e);
    });

    window.addEventListener('mouseup', function (e) {
        mouse.down = false;
        handleMouseEvent(e);
    });

    window.addEventListener('mouseout', function (e) {
        mouse.down = false;
        handleMouseEvent(e);
    });

    window.addEventListener('mousemove',  handleMouseEvent );
};

Last word : performance testing an event handler is, to say the least, questionable, unless you can ensure that the very same moves/clicks are performed during each test. There no way to handle things faster than in the code above. Well, you might save 2 muls if you are sure canvas isn't css scaled, but anyway as of now the browser overhead for input handling is so big that it won't change a thing.

like image 21
GameAlchemist Avatar answered Sep 19 '22 08:09

GameAlchemist