JavaScript如何使图像始终朝向鼠标光标旋转?

9

我正在尝试在JavaScript中使箭头指向鼠标光标。目前它只是疯狂地旋转,而不是指向光标。

这里有我的代码fiddle链接: https://jsfiddle.net/pk1w095s/

以下是代码本身:

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
};
2个回答

11

首先,去掉角度转换 - Math.atan2ctx.rotate函数都使用弧度。

这样修复了旋转问题 - 接下来仍然存在一些数学错误,最容易解决的方法是将绘图与数学分开处理。

下面的函数绘制给定角度旋转的箭头:

// 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();
}

同时onmove处理程序只是计算方向。

document.onmousemove = function(e) {
    var dx = e.pageX - centerX;
    var dy = e.pageY - centerY;
    var theta = Math.atan2(dy, dx);
    drawArrow(theta);
};

注意,画布的Y轴朝下(与正常笛卡尔坐标相反),因此旋转最终变成顺时针而不是逆时针。

可在https://jsfiddle.net/alnitak/5vp0syn5/上查看示例。


1
比我的好多了 :-) - JonSG
抱歉(-1),但是你的回答有很多问题。请看我的答案,了解你做错了什么。 - Blindman67

8

最佳实践的解决方案。

现有(Alnitak的)答案存在一些问题:

  • 计算中的符号错误,然后进行了太多的调整来纠正错误的符号。
  • 箭头没有指向鼠标,因为鼠标坐标不正确。试着将鼠标移动到接受的(Alnitak的)答案的箭头尖端,你会发现它只在画布上的两个点处起作用。需要校正鼠标对画布填充/偏移的影响。
  • 画布坐标需要包括页面滚动位置,因为鼠标事件的pageXpageY属性是相对于页面左上角而非整个文档的。如果您不调整滚动条,则当您滚动页面时,箭头将不再指向鼠标。或者,您可以使用鼠标事件的clientX、clientY属性,它们保存鼠标相对于客户端(整体)页面左上角的坐标值,这样您就不需要进行滚动调整。
  • 使用"save"和"restore"是低效的。应该使用"setTransform"
  • 没有必要时进行渲染。鼠标事件触发的次数要比屏幕刷新的次数多得多。当鼠标事件触发时进行渲染只会产生永远不会被看到的渲染。渲染既消耗计算资源,也消耗电量。不必要的渲染会快速耗尽设备的电池。

以下是一个"最佳实践"的解决方案。

核心函数绘制一个图像,查看点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
}

此示例展示如何使用 requestAnimationFrame 来确保只在DOM准备好渲染时进行渲染,并使用 getBoundingClientRect 获取相对于画布的鼠标位置。

左上角的计数器显示有多少无需渲染的鼠标事件已触发。当鼠标移动得非常缓慢时,计数器不会增加;而正常速度移动鼠标时,您将看到每几秒钟就可以生成数百个不必要的渲染事件。第二个数字是节省时间的大约值(以1/1000秒为单位),% 是节省时间与渲染时间的比率。

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


我的答案中没有数学“猜测”,它只是“纠正”了与普通笛卡尔坐标相比的倒置Y轴(尽管有些否定确实互相抵消)。至于你提到的其他问题-没错,但那些问题远远超出了原帖作者试图解决的问题。 - Alnitak
@alnitak,你的解决方案不起作用,箭头只沿着y = x线指向鼠标。此外,从mousemove事件进行渲染是非常糟糕的做法,不应该被表示为OP问题的解决方案。动画应该通过requestAnimationFrame完成,并且由于OP正在创建动画,因此它是答案的重要部分。 - Blindman67
它在这里运行良好 - 任何其他“问题”都是从OP的代码和/或环境继承而来的。我所做的就是按照要求修复他的数学问题。 - Alnitak
@Alnitak 只需在鼠标事件中添加 canvas.getBoundingClientRect() 调用并修复偏移即可! - Blindman67

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接