Real Mouse Position in Canvas

Real mouse position in canvas

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.

Convert mouse position to Canvas Coordinates and back

Here's a code snippet that seems to be working, you can probably adapt it for your purposes.

What I used was:

function toCanvasCoords(pageX, pageY, scale) {
var rect = canvas.getBoundingClientRect();
let x = (pageX - rect.left) / scale;
let y = (pageY - rect.top) / scale;
return toPoint(x, y);
}

and

function toScreenCoords(x, y, scale) {
var rect = canvas.getBoundingClientRect();
let wx = x * scale + rect.left + scrollElement.scrollLeft;
let wy = y * scale + rect.top + scrollElement.scrollTop;
return toPoint(wx, wy);
}

I'm just getting the mouse position from the window object. I'm may be mistaken, but I think this is why scrollLeft and scrollTop don't appear in toCanvasCoords (since the position is relative to the client area of the window itself, the scroll doesn't come into it). But then when you transform back, you have to take it into account.

This ultimately just returns the mouse position relative to the window (which was the input), so it's not really necessary to do the whole transformation in a roundabout way if you just want to attach an element to the mouse pointer. But transforming back is useful if you want to have something attached to a certain point on the canvas image (say, a to feature on the map) - which I'm guessing is something that you're going for, since you said that you want to render markers on an overlay div.

In the code snippet bellow, the red circle is drawn on the canvas itself at the location returned by toCanvasCoords; you'll notice that it scales together with the background.

I didn't use an overlay div covering the entire map, I just placed a couple of small divs on top using absolute positioning. The black triangle is a div (#tracker) that basically tracks the mouse; it is placed at the result of toScreenCoords. It serves as a way to check if the transformations work correctly. It's an independent element, so it doesn't scale with the image.

The red triangle is another such div (#feature), and demonstrates the aforementioned "attach to feature" idea. Suppose the background is a something like a map, and suppose you want to attach a "map pin" icon to something on it, like to a particular intersection; you can take that location on the map (which is a fixed value), and pass it to toScreenCoords. In the code snippet below, I've aligned it with a corner of a square on the background, so that you can track it visually as you change scale and/or scroll. (After you click "Run code snippet", you can click "Full page", and then resize the window to get the scroll bars).

Now, depending on what exactly is going on in your code, you may have tweak a few things, but hopefully, this will help you. If you run into problems, make use of console.log and/or place some debug elements on the page that will display values live for you (e.g. mouse position, client rectangle, etc.), so that you can examine values. And take things one step at the time - e.g. first get the scale to work, but ignore scrolling, then try to get scrolling to work, but keep the scale at 1, etc.

const canvas = document.getElementById('canvas');
const context = canvas.getContext("2d");
const tracker = document.getElementById('tracker');
const feature = document.getElementById('feature');
const slider = document.getElementById("scale-slider");
const scaleDisplay = document.getElementById("scale-display");
const scrollElement = document.querySelector('html');

const bgImage = new Image();
bgImage.src = "https://i.stack.imgur.com/yxtqw.jpg"
var bgImageLoaded = false;
bgImage.onload = () => { bgImageLoaded = true; };

var mousePosition = toPoint(0, 0);
var scale = 1;

function updateMousePosition(evt) {
mousePosition = toPoint(evt.clientX, evt.clientY);
}

function getScale(evt) {
scale = evt.target.value;
scaleDisplay.textContent = scale;
}

function toCanvasCoords(pageX, pageY, scale) {
var rect = canvas.getBoundingClientRect();
let x = (pageX - rect.left) / scale;
let y = (pageY - rect.top) / scale;
return toPoint(x, y);
}

function toScreenCoords(x, y, scale) {
var rect = canvas.getBoundingClientRect();
let wx = x * scale + rect.left + scrollElement.scrollLeft;
let wy = y * scale + rect.top + scrollElement.scrollTop;
return toPoint(wx, wy);
}

function toPoint(x, y) {
return { x: x, y: y }
}

function roundPoint(point) {
return {
x: Math.round(point.x),
y: Math.round(point.y)
}
}

function update() {
context.clearRect(0, 0, 500, 500);
context.save();
context.scale(scale, scale);

if (bgImageLoaded)
context.drawImage(bgImage, 0, 0);

const canvasCoords = toCanvasCoords(mousePosition.x, mousePosition.y, scale);
drawTarget(canvasCoords);

const trackerCoords = toScreenCoords(canvasCoords.x, canvasCoords.y, scale);
updateTrackerLocation(trackerCoords);

updateFeatureLocation()

context.restore();
requestAnimationFrame(update);
}

function drawTarget(location) {
context.fillStyle = "rgba(255, 128, 128, 0.8)";
context.beginPath();
context.arc(location.x, location.y, 8.5, 0, 2*Math.PI);
context.fill();
}

function updateTrackerLocation(location) {
const canvasRectangle = offsetRectangle(canvas.getBoundingClientRect(),
scrollElement.scrollLeft, scrollElement.scrollTop);
if (rectContains(canvasRectangle, location)) {
tracker.style.left = location.x + 'px';
tracker.style.top = location.y + 'px';
}
}

function updateFeatureLocation() {
// suppose the background is a map, and suppose there's a feature of interest
// (e.g. a road intersection) that you want to place the #feature div over
// (I roughly aligned it with a corner of a square).
const featureLoc = toScreenCoords(84, 85, scale);
feature.style.left = featureLoc.x + 'px';
feature.style.top = featureLoc.y + 'px';
}

function offsetRectangle(rect, offsetX, offsetY) {
// copying an object via the spread syntax or
// using Object.assign() doesn't work for some reason
const result = JSON.parse(JSON.stringify(rect));
result.left += offsetX;
result.right += offsetX;
result.top += offsetY;
result.bottom += offsetY;
result.x = result.left;
result.y = result.top;

return result;
}

function rectContains(rect, point) {
const inHorizontalRange = rect.left <= point.x && point.x <= rect.right;
const inVerticalRange = rect.top <= point.y && point.y <= rect.bottom;
return inHorizontalRange && inVerticalRange;
}

window.addEventListener('mousemove', (e) => updateMousePosition(e), false);
slider.addEventListener('input', (e) => getScale(e), false);
requestAnimationFrame(update);
#canvas {
border: 1px solid gray;
}

#tracker, #feature {
position: absolute;
left: 0;
top: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 10px solid black;
transform: translate(-4px, 0);
}

#feature {
border-bottom: 10px solid red;
}
<div>
<label for="scale-slider">Scale:</label>
<input type="range" id="scale-slider" name="scale-slider" min="0.5" max="2" step="0.02" value="1">
<span id="scale-display">1</span>
</div>
<canvas id="canvas" width="500" height="500"></canvas>
<div id="tracker"></div>
<div id="feature"></div>

Get the mouse coordinates when clicking on canvas

You could achieve that by using the offsetX and offsetY property of MouseEvent

//setup canvas

var canvasSetup = document.getElementById("puppyCanvas");

var ctx = canvasSetup.getContext("2d");

guessX = 0; //stores user's click on canvas

guessY = 0; //stores user's click on canvas

function storeGuess(event) {

var x = event.offsetX;

var y = event.offsetY;

guessX = x;

guessY = y;

console.log("x coords: " + guessX + ", y coords: " + guessY);

}
<canvas id="puppyCanvas" width="500" height="500" style="border:2px solid black" onclick="storeGuess(event)"></canvas>

Canvas doesn't follow actual position of mouse

CSS to canvas pixel coords

Canvas resolution, measured in pixels, and canvas page size, measured in CSS pixels, are independent.

When using the 2D API rendering is done in canvas pixels (not CSS pixels)

The mouse event holds coordinates as CSS pixels and as such will not always directly translate to canvas pixel coordinates.

One can get the page size of the canvas using getBoundingClientRect which can be used to scale from CSS pixels to canvas pixels.

However getBoundingClientRect includes the element's padding and border which must also be considered when scaling the CSS pixels to canvas pixels.

Note in my comments I talked about the device pixel ratio. I was wrong in my assessment of it's use, it is not required in this situation

Example

The example uses a default canvas (300 by 150px) and then uses CSS rules to scale it to the page, adding padding and a border in units other than CSS pixels. The canvas is also scaled such that it can be scrolled up and down.

Switch between the full page and snippet to see how scaling effects the example.

The function getInnerRect uses getBoundingClientRect and getComputedStyle to get the bounds of the canvas on the page in CSS pixels. From the top left of the top left pixel on the canvas to the bottom right of the bottom right most pixel.

The draw function then uses the inner rect (named bounds in example) to calculate the scales (x, y). Then offsets the mouse coordinates and scales them to draw to the canvas.

  • Note That the function mouseEvent captures the mouse coordinates from event.pageX and event.pageY

  • Note The function getBoundingClientRect is relative to the page scroll position. Thus when using it's return one must adjust the coordinates to take in acount the page scroll position. The example uses scrollX and scrollY to do this.

  • Note that the scale and offset can also be encapsulated as a 2D transformation. As the coordinates in the example will only change when the page is resized the transform can be calculated once on resize and applied to the canvas. Thus the draw function can use the CSS pixels to render directly to the canvas. This make the code in the example a little simpler. (See second snippet);

  • Note That because the transform will transform all rendering sizes it is necessary to compute the inverse scale of the transform. This is used to scale the line width in the second example. If this is not done then the line width rendered will change as the page size changes.

const ctx = canvas.getContext("2d");
var bounds;
const mouse = {x:0,y:0,b:0,px:0,py:0}; // p for previouse b for button
function mouseEvent(event) {
mouse.px = mouse.x;
mouse.py = mouse.y;
mouse.x = event.pageX;
mouse.y = event.pageY;
if (event.type === "mousedown") {
mouse.b = true;
canvas.classList.add("drawing");
} else if (event.type === "mouseup" || event.type === "mouseout") {
mouse.b = false;
canvas.classList.remove("drawing");
}
draw();
}
addEventListener("mousemove", mouseEvent);
addEventListener("mousedown", mouseEvent);
addEventListener("mouseup", mouseEvent);
addEventListener("mouseout", mouseEvent);

const CSSPx2Number = cssPx => Number(cssPx.replace("px",""));
function getInnerRect(element) {
var top, left, right, bottom;
const bounds = element.getBoundingClientRect();
const canStyle = getComputedStyle(element);

left = CSSPx2Number(canStyle.paddingLeft);
left += CSSPx2Number(canStyle.borderLeftWidth);

top = CSSPx2Number(canStyle.paddingTop);
top += CSSPx2Number(canStyle.borderTopWidth);

right = CSSPx2Number(canStyle.paddingRight);
right += CSSPx2Number(canStyle.borderRightWidth);

bot = CSSPx2Number(canStyle.paddingBottom);
bot += CSSPx2Number(canStyle.borderBottomWidth);

return {
top: bounds.top + top + scrollY,
left: bounds.left + left + scrollX,
bottom: bounds.bottom + bot + scrollY,
right: bounds.right + right + scrollX,
width: bounds.width - left - right,
height: bounds.height - top - bot,
};

}
function resizeEvent() {
bounds = getInnerRect(canvas);
widthText.textContent = "Canvas page size: " + bounds.width.toFixed(1) + " by " + bounds.height.toFixed(1) + "px Canvas width: " + canvas.width + " by " + canvas.height + "px";
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.lineCap = ctx.lineJoin = "round";
}
addEventListener("resize",resizeEvent);
resizeEvent();

function draw() {
if (mouse.b) {
const xScale = canvas.width / bounds.width;
const yScale = canvas.height / bounds.height;

const x = (mouse.x - bounds.left) * xScale;
const y = (mouse.y - bounds.top) * yScale;
const px = (mouse.px - bounds.left) * xScale;
const py = (mouse.py - bounds.top) * yScale;

ctx.beginPath();
ctx.lineTo(px, py);
ctx.lineTo(x, y);
ctx.stroke();
}
}
canvas {
position: absolute;
top: 5%;
left: 10%;
width: 80%;
height: 170%;
border: 1pc solid blue;
padding: 1%;
cursor: crosshair;
}
.drawing { cursor: none; }
.info {
position: absolute;
font-family: arial;
font-size: x-small;
pointer-events: none;
text-align: center;
background: white;
border: 1px solid black;

}
#widthText {
top: 2%;
left: 20%;
width: 60%;
}
<!-- default size of canvas is 300 by 150 -->
<canvas id="canvas"></canvas>
<div id="widthText" class="info"></div>

How do I get the coordinates of a mouse click on a canvas element?

If you like simplicity but still want cross-browser functionality I found this solution worked best for me. This is a simplification of @Aldekein´s solution but without jQuery.

function getCursorPosition(canvas, event) {
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
console.log("x: " + x + " y: " + y)
}

const canvas = document.querySelector('canvas')
canvas.addEventListener('mousedown', function(e) {
getCursorPosition(canvas, e)
})

Getting the exact mouse position on canvas inside divisons

You need to scale the mouse coordinates to match the canvas resolution.

// your code
var mouseX = evt.clientX - left + window.pageXOffset;
var mouseY = evt.clientY - top + window.pageYOffset;

// Add the following 3 lines to scale the mouse coordinates to the
// canvas resolution
const bounds = canvas_test.getBoundingClientRect();
mouseX = (mouseX / bounds.width) * canvas_test.width;
mouseY = (mouseY / bounds.height) * canvas_test.height;

// your code
return {
x:mouseX,
y:mouseY
};

Getting mouse location in canvas

Easiest way is probably to add a onmousemove event listener to the canvas element, and then you can get the coordinates relative to the canvas from the event itself.

This is trivial to accomplish if you only need to support specific browsers, but there are differences between f.ex. Opera and Firefox.

Something like this should work for those two:

function mouseMove(e)
{
var mouseX, mouseY;

if(e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
else if(e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}

/* do something with mouseX/mouseY */
}

Make mouse coordinates relative to canvas coordinates

To convert canvas mouse coordinates to relative coordinates in the range of -1 to +1, one first needs to get the relative pixel coordinates within the canvas using offsetX/offsetY provided by the mouse event.

Then divide this by the actual width and height of the canvas using
clientWidth/clientHeight.

Having the relative coordinates in a range from 0 to 1 we can now move them into the right range by multiplying by 2 and subtracting 1.

Note that in WebGL the up axis is positive 1 so one wants to invert the Y coordinate.

All put together:

var ndcX = (mouseEvent.offsetX / canvas.clientWidth) * 2 - 1;
var ndcY = (1 - (mouseEvent.offsetY / canvas.clientHeight)) * 2 - 1;

How to get Mouse Position on Transformed HTML5 Canvas

I don't know exactly what you have tried so far, but for a basic mouse coordinate to transformed canvas (non skewed), you'll have to do

mouseX = (evt.clientX - canvas.offsetLeft - translateX) / scaleX;
mouseY = (evt.clientY - canvas.offsetTop - translateY) / scaleY;

But canvas.offsetXXX doesn't take scroll amount into account, so this demo uses getBoundingRect instead.

var ctx = canvas.getContext('2d');

window.addEventListener('resize', resize);

// you probably have these somewhere

var maxScreenWidth = 1800,

maxScreenHeight = 1200,

scaleFillNative, screenWidth, screenHeight;

// you need to set available to your mouse move listener

var translateX, translateY;

function resize() {

screenWidth = window.innerWidth;

screenHeight = window.innerHeight;

// here you set scaleX and scaleY to the same variable

scaleFillNative = Math.max(screenWidth / maxScreenWidth, screenHeight / maxScreenHeight);

canvas.width = screenWidth;

canvas.height = screenHeight;

// store these values

translateX = Math.floor((screenWidth - (maxScreenWidth * scaleFillNative)) / 2);

translateY = Math.floor((screenHeight - (maxScreenHeight * scaleFillNative)) / 2);

ctx.setTransform(scaleFillNative, 0, 0, scaleFillNative, translateX, translateY);

}

window.addEventListener('mousemove', mousemoveHandler, false);

function mousemoveHandler(e) {

// Note : I don't think there is any event default on mousemove, no need to prevent it

// normalize our event's coordinates to the canvas current transform

// here we use .getBoundingRect() instead of .offsetXXX

// because we also need to take scroll into account,

// in production, store it on debounced(resize + scroll) events.

var rect = canvas.getBoundingClientRect();

var mouseX = (e.clientX - rect.left - translateX) / scaleFillNative,

mouseY = (e.clientY - rect.top - translateY) / scaleFillNative;

ctx.fillRect(mouseX - 5, mouseY - 5, 10, 10);

}

// an initial call

resize();
<canvas id="canvas"></canvas>


Related Topics



Leave a reply



Submit