I'm trying to get an arrow to point at my mouse cursor in javascript. Right now it just spins around violently, instead of pointing at the cursor.
Here is a fiddle of my code: https://jsfiddle.net/pk1w095s/
And here is the code its self:
var cv = document.createElement('canvas');
cv.width = 1224;
cv.height = 768;
document.body.appendChild(cv);
var rotA = 0;
var ctx = cv.getContext('2d');
var arrow = new Image();
var cache;
arrow.onload = function() {
cache = this;
ctx.drawImage(arrow, cache.width/2, cache.height/2);
};
arrow.src = 'https://d30y9cdsu7xlg0.cloudfront.net/png/35-200.png';
var cursorX;
var cursorY;
document.onmousemove = function(e) {
cursorX = e.pageX;
cursorY = e.pageY;
ctx.save(); //saves the state of canvas
ctx.clearRect(0, 0, cv.width, cv.height); //clear the canvas
ctx.translate(cache.width, cache.height); //let's translate
var centerX = cache.x + cache.width / 2;
var centerY = cache.y + cache.height / 2;
var angle = Math.atan2(e.pageX - centerX, -(e.pageY - centerY)) * (180 / Math.PI);
ctx.rotate(angle);
ctx.drawImage(arrow, -cache.width / 2, -cache.height / 2, cache.width, cache.height); //draw the image
ctx.restore(); //restore the state of canvas
};
In the first instance, get rid of the conversion to degrees - both the Math.atan2
and the ctx.rotate
functions take radians.
That fixes the wild rotation - you still then have some math errors, which are most easily sorted out by splitting out the drawing from the math.
The function below draws the arrow rotated by the given angle:
// NB: canvas rotations go clockwise
function drawArrow(angle) {
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(-Math.PI / 2); // correction for image starting position
ctx.rotate(angle);
ctx.drawImage(arrow, -arrow.width / 2, -arrow.height / 2);
ctx.restore();
}
and the onmove
handler just figures out the direction.
document.onmousemove = function(e) {
var dx = e.pageX - centerX;
var dy = e.pageY - centerY;
var theta = Math.atan2(dy, dx);
drawArrow(theta);
};
Note that on a canvas the Y axis points downwards (contrary to normal cartesian coordinates) so the rotations end up going clockwise instead of anti-clockwise.
working demo at https://jsfiddle.net/alnitak/5vp0syn5/
As the existing (Alnitak's) answer has some issues.
pageX
, pageY
properties are relative to the top left of the page, not the whole document. If you scroll the page the arrow will no longer point at the mouse if you don't. Or you can use the mouse event clientX, clientY properties that hold the mouse coordinates to the client (whole) page top left thus you dont need to correct for scroll.Here is a "Best practice" solution.
The core function draws an image looking at a point lookx
,looky
var drawImageLookat(img, x, y, lookx, looky){
ctx.setTransform(1, 0, 0, 1, x, y); // set scale and origin
ctx.rotate(Math.atan2(looky - y, lookx - x)); // set angle
ctx.drawImage(img,-img.width / 2, -img.height / 2); // draw image
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default not needed if you use setTransform for other rendering operations
}
The demo show how to use requestAnimationFrame
to ensure you only render when the DOM is ready to render, Use getBoundingClientRect
to get the mouse position relative to the canvas.
The counter at top left show how many mouse events have fired that did not need to be rendered. Move the mouse very slowly and the counter will not increase. Move the mouse at a normal speed and you will see that you can generate 100's of unneeded render events every few seconds. The second number is the approximate time saved in 1/1000th seconds, and the % is ratio time saved over time to render.
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = 512;
canvas.height = 512;
canvas.style.border = "1px solid black";
document.body.appendChild(canvas);
var renderSaveCount = 0; // Counts the number of mouse events that we did not have to render the whole scene
var arrow = {
x : 256,
y : 156,
image : new Image()
};
var mouse = {
x : null,
y : null,
changed : false,
changeCount : 0,
}
arrow.image.src = 'https://d30y9cdsu7xlg0.cloudfront.net/png/35-200.png';
function drawImageLookat(img, x, y, lookx, looky){
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.rotate(Math.atan2(looky - y, lookx - x) - Math.PI / 2); // Adjust image 90 degree anti clockwise (PI/2) because the image is pointing in the wrong direction.
ctx.drawImage(img, -img.width / 2, -img.height / 2);
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default not needed if you use setTransform for other rendering operations
}
function drawCrossHair(x,y,color){
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(x - 10, y);
ctx.lineTo(x + 10, y);
ctx.moveTo(x, y - 10);
ctx.lineTo(x, y + 10);
ctx.stroke();
}
function mouseEvent(e) { // get the mouse coordinates relative to the canvas top left
var bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left;
mouse.y = e.pageY - bounds.top;
mouse.cx = e.clientX - bounds.left; // to compare the difference between client and page coordinates
mouse.cy = e.clienY - bounds.top;
mouse.changed = true;
mouse.changeCount += 1;
}
document.addEventListener("mousemove",mouseEvent);
var renderTimeTotal = 0;
var renderCount = 0;
ctx.font = "18px arial";
ctx.lineWidth = 1;
// only render when the DOM is ready to display the mouse position
function update(){
if(arrow.image.complete && mouse.changed){ // only render when image ready and mouse moved
var now = performance.now();
mouse.changed = false; // flag that the mouse coords have been rendered
ctx.clearRect(0, 0, canvas.width, canvas.height);
// get mouse canvas coordinate correcting for page scroll
var x = mouse.x - scrollX;
var y = mouse.y - scrollY;
drawImageLookat(arrow.image, arrow.x, arrow.y, x ,y);
// Draw mouse at its canvas position
drawCrossHair(x,y,"black");
// draw mouse event client coordinates on canvas
drawCrossHair(mouse.cx,mouse.cy,"rgba(255,100,100,0.2)");
// draw line from arrow center to mouse to check alignment is perfect
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.globalAlpha = 0.2;
ctx.moveTo(arrow.x, arrow.y);
ctx.lineTo(x, y);
ctx.stroke();
ctx.globalAlpha = 1;
// Display how many renders that were not drawn and approx how much time saved (excludes DOM time to present canvas to display)
renderSaveCount += mouse.changeCount -1;
mouse.changeCount = 0;
var timeSaved = ((renderTimeTotal / renderCount) * renderSaveCount);
var timeRatio = ((timeSaved / renderTimeTotal) * 100).toFixed(0);
ctx.fillText("Avoided "+ renderSaveCount + " needless renders. Saving ~" + timeSaved.toFixed(0) +"ms " + timeRatio + "% .",10,20);
// get approx render time per frame
renderTimeTotal += performance.now()-now;
renderCount += 1;
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With