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.

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