Search code examples
javascriptjquerycsscanvashtml5-canvas

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
};

Solution

  • "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);