Real mouse position in canvas
Asked Answered
P

5

167

I'm trying to draw with the mouse over a HTML5 canvas, but the only way that it seems to work well is if the canvas is in the position 0,0 (upper left corner) if I change the canvas position, for some reason it doesn't draw like it should. Here is my code.

 function createImageOnCanvas(imageId){
    document.getElementById("imgCanvas").style.display = "block";
    document.getElementById("images").style.overflowY= "hidden";
    var canvas = document.getElementById("imgCanvas");
    var context = canvas.getContext("2d");
    var img = new Image(300,300);
    img.src = document.getElementById(imageId).src;
    context.drawImage(img, (0),(0));
}

function draw(e){
    var canvas = document.getElementById("imgCanvas");
    var context = canvas.getContext("2d");
    posx = e.clientX;
    posy = e.clientY;
    context.fillStyle = "#000000";
    context.fillRect (posx, posy, 4, 4);
}

The HTML part

 <body>
 <div id="images">
 </div>
 <canvas onmousemove="draw(event)" style="margin:0;padding:0;" id="imgCanvas"
          class="canvasView" width="250" height="250"></canvas> 

I have read there's a way of creating a simple function in JavaScript to get the right position, but I have no idea about how to do it.

Pivot answered 16/6, 2013 at 5:13 Comment(0)
C
334

The Simple 1:1 Scenario

For situations where the canvas element is 1:1 compared to the bitmap size, you can get the mouse positions by using this snippet:

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

Just call it from your event with the event and canvas as arguments. It returns an object with x and y for the mouse positions.

As the mouse position you are getting is relative to the client window you’ll have to subtract the position of the canvas element to convert it relative to the element itself.

Example of integration in your code:

// put this outside the event loop..
var canvas = document.getElementById("imgCanvas");
var context = canvas.getContext("2d");

function draw(evt) {
  var pos = getMousePos(canvas, evt);

  context.fillStyle = "#000000";
  context.fillRect (pos.x, pos.y, 4, 4);
}

Note: borders and padding will affect position if applied directly to the canvas element so these needs to be considered via getComputedStyle() – or apply those styles to a parent div instead.

When Element and Bitmap are of different sizes

When there is the situation of having the element at a different size than the bitmap itself, for example, the element is scaled using CSS or there is pixel-aspect ratio etc. you will have to address this.

Example:

function  getMousePos(canvas, evt) {
  var rect = canvas.getBoundingClientRect(), // abs. size of element
    scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for x
    scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for y

  return {
    x: (evt.clientX - rect.left) * scaleX,   // scale mouse coordinates after they have
    y: (evt.clientY - rect.top) * scaleY     // been adjusted to be relative to element
  }
}

With transformations applied to context (scale, rotation etc.)

Then there is the more complicated case where you have applied transformation to the context such as rotation, skew/shear, scale, translate etc. To deal with this you can calculate the inverse matrix of the current matrix.

Newer browsers let you read the current matrix via the currentTransform property and Firefox (current alpha) even provide an inverted matrix through the mozCurrentTransformInverted. Firefox however, via mozCurrentTransform, will return an Array and not DOMMatrix as it should. Neither Chrome, when enabled via experimental flags, will return a DOMMatrix but a SVGMatrix.

In most cases however you will have to implement a custom matrix solution of your own (such as my own solution here – free/MIT project) until this get full support.

When you eventually have obtained the matrix regardless of path you take to obtain one, you’ll need to invert it and apply it to your mouse coordinates. The coordinates are then passed to the canvas which will use its matrix to convert it to back wherever it is at the moment.

This way the point will be in the correct position relative to the mouse. Also here you need to adjust the coordinates (before applying the inverse matrix to them) to be relative to the element.

An example just showing the matrix steps:

function draw(evt) {
  var pos = getMousePos(canvas, evt);        // get adjusted coordinates as above
  var imatrix = matrix.inverse();            // get inverted matrix somehow
  pos = imatrix.applyToPoint(pos.x, pos.y);  // apply to adjusted coordinate
    
  context.fillStyle = "#000000";
  context.fillRect(pos.x-1, pos.y-1, 2, 2);
}

An example of using currentTransform when implemented would be:

  var pos = getMousePos(canvas, e);          // get adjusted coordinates as above
  var matrix = ctx.currentTransform;         // W3C (future)
  var imatrix = matrix.invertSelf();         // invert

  // apply to point:
  var x = pos.x * imatrix.a + pos.y * imatrix.c + imatrix.e;
  var y = pos.x * imatrix.b + pos.y * imatrix.d + imatrix.f;

Update: I made a free solution (MIT) to embed all these steps into a single easy-to-use object that can be found here and also takes care of a few other nitty-gritty things most ignore.

Cacilia answered 16/6, 2013 at 5:16 Comment(16)
getBoundingRectangle() gives more exact co-ordinates than canvas.getOffset();Chutzpah
Um, these coordinates aren't perfect. I get wrong values on the mobile phone. Does someone know a fix or where to look?Hooked
@Hooked for fix check little modification I've provided in here. It takes into account that canvas size may differ from its style size, which may be a your issue.Exploration
I updated answer to address scaling problems.Cacilia
getTransform() seem to be what the standard lands on.Cacilia
This works great until the canvas has some of it (bottom, left) outside the viewing area. In that case, the click is offset by the distance outside the view rect. I am still looking for a valid answer to this question, but will respond here with info once I do.Lavonlavona
There are tutorials out there that use pageX and pageY ... do not do this. Use clientX and clientY as shown in the code above. If you use pageX and pageY, scrolling the page will cause the click to be offset by the amount you've scrolled... I had this problem, and the code above seemed (ahem) to be exactly like mine...Lavonlavona
i'm in case 1:1 scenario and i use : var bRect = theCanvas.getBoundingClientRect(); mouseX = (evt.clientX - bRect.left)*(theCanvas.width/bRect.width); mouseY = (evt.clientY - bRect.top)*(theCanvas.height/bRect.height); It work all browser, safari iOS except case In chrome in android device. what is wrong?Orphrey
@Orphrey hard to tell, the simple explanation is of course that chrome on android lack some feature support. Could you setup a fiddle with the code you're using?Cacilia
I want draw image with X,Y mouse when i touch. but it draw in other sideOrphrey
All the links now seem to be deadBumgarner
i keep getting clientX undefinedSuperpower
This post was very helpful for me. I've created a JSFiddle that pulls it all together: jsfiddle.net/mattdeeds/yqLvza57/37Thermocouple
I owe you my lifeHimmler
currentTransform is deprecated and mostly unsupported: developer.mozilla.org/en-US/docs/Web/API/…Bobbysoxer
In 2023 you can use this const rect = canvasEl.getBoundingClientRect();const p = canvasCtx.getTransform().inverse().transformPoint(new DOMPoint((event.clientX - rect.left) / rect.width * parseInt(canvasEl.width),(event.clientY - rect.top) / rect.height * parseInt(canvasEl.height)));Weitzel
H
66

You can get the mouse positions by using this snippet:

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

This code takes into account both changing coordinates to canvas space (evt.clientX - rect.left) and scaling when canvas logical size differs from its style size (/ (rect.right - rect.left) * canvas.width see: Canvas width and height in HTML5).

Example: http://jsfiddle.net/sierawski/4xezb7nL/

Source: jerryj comment on http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/

Harrisonharrod answered 11/10, 2015 at 8:59 Comment(5)
I had to use this version, since my canvas size changed while scaling and/or resizing the window. I had already solved the problem by scaling with the computed width & height using getComputedStyle(), but this solution is more elegant. I would recommend this answer to those who want a more general solution.Admire
It assumes there's no border on the canvas (if there is it's a PITA so best not to put borders on canvases)Xanthic
@Rafal S : I use var bRect = theCanvas.getBoundingClientRect(); mouseX = (evt.clientX - bRect.left)*(theCanvas.width/bRect.width); mouseY = (evt.clientY - bRect.top)*(theCanvas.height/bRect.height); In browser chrome, IE in PC , safari in iOS is ok, but in chrome web android is wrong mouseY so much. Can I fix it by your code?Orphrey
This is a great solution, but I just thought I might add a solution to scrolling pages with a canvas. This may not work in all situation, but it worked for my project: y: ((event.pageY - rect.top) / (rect.bottom - rect.top) * canvas.height) - window.scrollYMadalene
For some reason this ended up being a lot faster than i was previously doing. Thanks!Garrity
D
9

You need to get the mouse position relative to the canvas

To do that you need to know the X/Y position of the canvas on the page.

This is called the canvas’s “offset”, and here’s how to get the offset. (I’m using jQuery in order to simplify cross-browser compatibility, but if you want to use raw javascript a quick Google will get that too).

    var canvasOffset=$("#canvas").offset();
    var offsetX=canvasOffset.left;
    var offsetY=canvasOffset.top;

Then in your mouse handler, you can get the mouse X/Y like this:

  function handleMouseDown(e){
      mouseX=parseInt(e.clientX-offsetX);
      mouseY=parseInt(e.clientY-offsetY);
}

Here is an illustrating code and fiddle that shows how to successfully track mouse events on the canvas:

http://jsfiddle.net/m1erickson/WB7Zu/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

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

    var canvasOffset=$("#canvas").offset();
    var offsetX=canvasOffset.left;
    var offsetY=canvasOffset.top;

    function handleMouseDown(e){
      mouseX=parseInt(e.clientX-offsetX);
      mouseY=parseInt(e.clientY-offsetY);
      $("#downlog").html("Down: "+ mouseX + " / " + mouseY);

      // Put your mousedown stuff here

    }

    function handleMouseUp(e){
      mouseX=parseInt(e.clientX-offsetX);
      mouseY=parseInt(e.clientY-offsetY);
      $("#uplog").html("Up: "+ mouseX + " / " + mouseY);

      // Put your mouseup stuff here
    }

    function handleMouseOut(e){
      mouseX=parseInt(e.clientX-offsetX);
      mouseY=parseInt(e.clientY-offsetY);
      $("#outlog").html("Out: "+ mouseX + " / " + mouseY);

      // Put your mouseOut stuff here
    }

    function handleMouseMove(e){
      mouseX=parseInt(e.clientX-offsetX);
      mouseY=parseInt(e.clientY-offsetY);
      $("#movelog").html("Move: "+ mouseX + " / " + mouseY);

      // Put your mousemove stuff here

    }

    $("#canvas").mousedown(function(e){handleMouseDown(e);});
    $("#canvas").mousemove(function(e){handleMouseMove(e);});
    $("#canvas").mouseup(function(e){handleMouseUp(e);});
    $("#canvas").mouseout(function(e){handleMouseOut(e);});

}); // end $(function(){});
</script>

</head>

<body>
    <p>Move, press and release the mouse</p>
    <p id="downlog">Down</p>
    <p id="movelog">Move</p>
    <p id="uplog">Up</p>
    <p id="outlog">Out</p>
    <canvas id="canvas" width=300 height=300></canvas>

</body>
</html>
Dodie answered 16/6, 2013 at 5:56 Comment(1)
Instead of $("#canvas").offset(); and then taking left and top i was using $("#canvas").offsetLeft , so the relative position was not right !Chutzpah
K
9

The easiest way to compute the correct mouse click or mouse move position on a canvas event is to use this little equation:

canvas.addEventListener('click', event =>
{
    let bound = canvas.getBoundingClientRect();

    let x = event.clientX - bound.left - canvas.clientLeft;
    let y = event.clientY - bound.top - canvas.clientTop;

    context.fillRect(x, y, 16, 16);
});

If the canvas has padding-left or padding-top, subtract x and y via:

x -= parseFloat(style['padding-left'].replace('px'));
y -= parseFloat(style['padding-top'].replace('px'));

Klan answered 29/1, 2018 at 11:33 Comment(0)
T
0

I use this:

function geMousePosRelativeCanvas(event, canvas) {
    var mouseX = parseInt(event.offsetX * canvas.width / canvas.offsetWidth);
    var mouseY = parseInt(event.offsetY * canvas.height / canvas.offsetHeight);
    return {x: mouseX, y: mouseY};
}
Tableland answered 15/4 at 13:18 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Zion

© 2022 - 2024 — McMap. All rights reserved.