Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript making image rotate to always look at mouse cursor?

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
};
like image 886
daniel metlitski Avatar asked Oct 19 '16 01:10

daniel metlitski


2 Answers

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/

like image 175
Alnitak Avatar answered Oct 24 '22 23:10

Alnitak


"Best practice" solution.

As the existing (Alnitak's) answer has some issues.

  • Wrong sign in calculations, and then too many adjustments to correct for the wrong sign.
  • The arrow does not point at the mouse because the mouse coordinates are incorrect. Try to move the mouse to the tip of the arrow (of accepted (Alnitak's) answer) and you can see that it only works at two points on the canvas. The mouse needs to be corrected for the canvas padding/offset
  • Canvas coordinates need to include page scroll position because the mouse events 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.
  • Using save and restore is inefficient. Use setTransform
  • Rendering when not needed. The mouse fires many more time than the screen refreshes. Rendering when the mouse fires will will only result in renders that are never seen. Rendering is expensive in both processing and power use. Needless rendering will quickly drain a device's battery

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);
              
like image 37
Blindman67 Avatar answered Oct 24 '22 22:10

Blindman67