在原生JavaScript中获取画布中鼠标位置的最现代方法

Most modern method of getting mouse position within a canvas in native JavaScript

本文关键字:位置 方法 鼠标 布中 JavaScript 原生 获取      更新时间:2023-09-26

首先,我知道这个问题已经被问了很多次了。然而,提供的答案并不一致,并且使用了多种方法来获取鼠标位置。几个例子:

方法1:

canvas.onmousemove = function (event) { // this  object refers to canvas object  
    Mouse = {
        x: event.pageX - this.offsetLeft,
        y: event.pageY - this.offsetTop
    }
}

方法2:

function getMousePos(canvas, evt) {
    var rect = canvas.getBoundingClientRect();
    return {
        x: evt.clientX - rect.left,
        y: evt.clientY - rect.top
    };
}

方法3:

var findPos = function(obj) {
    var curleft = curtop = 0;
    if (obj.offsetParent) { 
        do {
            curleft += obj.offsetLeft;
            curtop += obj.offsetTop; 
        } while (obj = obj.offsetParent);
    }
    return { x : curleft, y : curtop };
};

方法4:

var x;
var y;
if (e.pageX || e.pageY)
{
    x = e.pageX;
    y = e.pageY;
}
else {
    x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 
} 
x -= gCanvasElement.offsetLeft;
y -= gCanvasElement.offsetTop;

等等

我好奇的是,就浏览器支持和在画布中获取鼠标位置的便利性而言,哪种方法最现代。还是那些影响很小的事情,以上任何一个都是好的选择?(是的,我意识到上面的代码并不完全相同)

这似乎有效。我认为这基本上就是K3N所说的。

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}
function getStyleSize(style, propName) {
  return parseInt(style.getPropertyValue(propName));
}
// assumes target or event.target is canvas
function getCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);
  // you can remove this if padding is 0. 
  // I hope this always returns "px"
  var style = window.getComputedStyle(target);
  var nonContentWidthLeft   = getStyleSize(style, "padding-left") +
                              getStyleSize(style, "border-left");
  var nonContentWidthTop    = getStyleSize(style, "padding-top") +
                              getStyleSize(style, "border-top");
  var nonContentWidthRight  = getStyleSize(style, "padding-right") +
                              getStyleSize(style, "border-right");
  var nonContentWidthBottom = getStyleSize(style, "padding-bottom") +
                              getStyleSize(style, "border-bottom");
  var rect = target.getBoundingClientRect();
  var contentDisplayWidth  = rect.width  - nonContentWidthLeft - nonContentWidthRight;
  var contentDisplayHeight = rect.height - nonContentWidthTop  - nonContentWidthBottom;
  pos.x = (pos.x - nonContentWidthLeft) * target.width  / contentDisplayWidth;
  pos.y = (pos.y - nonContentWidthTop ) * target.height / contentDisplayHeight;
  return pos;  
}

如果运行下面的示例并将鼠标移动到蓝色区域上,它将在光标下绘制。边框(黑色)、填充(红色)、宽度和高度都设置为非像素值。蓝色区域是实际画布像素。画布的分辨率没有设置,所以无论拉伸到什么大小,它都是300x150。

将鼠标移到蓝色区域上,它会在下面画一个像素。

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
function clearCanvas() {
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
clearCanvas();
var posNode = document.createTextNode("");
document.querySelector("#position").appendChild(posNode);
function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}
function getStyleSize(style, propName) {
  return parseInt(style.getPropertyValue(propName));
}
// assumes target or event.target is canvas
function getCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);
  
  // you can remove this if padding is 0. 
  // I hope this always returns "px"
  var style = window.getComputedStyle(target);
  var nonContentWidthLeft   = getStyleSize(style, "padding-left") +
                              getStyleSize(style, "border-left");
  var nonContentWidthTop    = getStyleSize(style, "padding-top") +
                              getStyleSize(style, "border-top");
  var nonContentWidthRight  = getStyleSize(style, "padding-right") +
                              getStyleSize(style, "border-right");
  var nonContentWidthBottom = getStyleSize(style, "padding-bottom") +
                              getStyleSize(style, "border-bottom");
  
  var rect = target.getBoundingClientRect();
  var contentDisplayWidth  = rect.width  - nonContentWidthLeft - nonContentWidthRight;
  var contentDisplayHeight = rect.height - nonContentWidthTop  - nonContentWidthBottom;
  pos.x = (pos.x - nonContentWidthLeft) * target.width  / contentDisplayWidth;
  pos.y = (pos.y - nonContentWidthTop ) * target.height / contentDisplayHeight;
  
  return pos;  
}
  
function handleMouseEvent(event) {
  var pos = getCanvasRelativeMousePosition(event);
  posNode.nodeValue = JSON.stringify(pos, null, 2);
  ctx.fillStyle = "white";
  ctx.fillRect(pos.x | 0, pos.y | 0, 1, 1);
}
canvas.addEventListener('mousemove', handleMouseEvent);
canvas.addEventListener('click', clearCanvas);
* {
  box-sizing: border-box;
  cursor: crosshair;
}
html, body {
  width: 100%;
  height: 100%;
  color: white;
}
.outer {
  background-color: green;
  display: flex;
  display: -webkit-flex;
  
  -webkit-justify-content: center;
  -webkit-align-content: center;
  -webkit-align-items: center;
  justify-content: center;
  align-content: center;
  align-items: center;  
  
  width: 100%;
  height: 100%;
}
.inner {
  border: 1em solid black;
  background-color: red;
  padding: 1.5em;
  width: 90%;
  height: 90%;
}
#position {
  position: absolute;
  left: 1em;
  top: 1em;
  z-index: 2;
  pointer-events: none;
}
<div class="outer">
  <canvas class="inner"></canvas>
</div>
<pre id="position"></pre>

那么,最好的建议是什么,除非你想完成所有这些步骤,否则画布的边框和填充总是为0。如果边框和填充为零,那么在下面的示例中,contentDisplayWidthcontentDisplayHeight只能使用canvas.clientWidthcanvas.clientHeight,并且所有nonContextXXX值都变为0。

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}
// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);
  pos.x = pos.x * target.width  / canvas.clientWidth;
  pos.y = pos.y * target.height / canvas.clientHeight;
  return pos;  
}

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
function clearCanvas() {
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
clearCanvas();
var posNode = document.createTextNode("");
document.querySelector("#position").appendChild(posNode);
function getRelativeMousePosition(event, target) {
  target = target || event.target;
  var rect = target.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}
// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  var pos = getRelativeMousePosition(event, target);
  
  pos.x = pos.x * target.width  / canvas.clientWidth;
  pos.y = pos.y * target.height / canvas.clientHeight;
  
  return pos;  
}
  
function handleMouseEvent(event) {
  var pos = getNoPaddingNoBorderCanvasRelativeMousePosition(event);
  posNode.nodeValue = JSON.stringify(pos, null, 2);
  ctx.fillStyle = "white";
  ctx.fillRect(pos.x | 0, pos.y | 0, 1, 1);
}
canvas.addEventListener('mousemove', handleMouseEvent);
canvas.addEventListener('click', clearCanvas);
* {
  box-sizing: border-box;
  cursor: crosshair;
}
html, body {
  width: 100%;
  height: 100%;
  color: white;
}
.outer {
  background-color: green;
  display: flex;
  display: -webkit-flex;
  
  -webkit-justify-content: center;
  -webkit-align-content: center;
  -webkit-align-items: center;
  justify-content: center;
  align-content: center;
  align-items: center;  
  
  width: 100%;
  height: 100%;
}
.inner {
  background-color: red;
  width: 90%;
  height: 80%;
  display: block;
}
#position {
  position: absolute;
  left: 1em;
  top: 1em;
  z-index: 2;
  pointer-events: none;
}
<div class="outer">
  <canvas class="inner"></canvas>
</div>
<pre id="position"></pre>

您的目标是画布,所以您只针对最近的浏览器
因此,您可以忘记方法4中的pageX内容。
方法1在嵌套画布的情况下失败
方法3和方法2一样,但由于是手工操作,所以速度较慢。

-->>选择2。

现在,由于您担心性能,所以不想调用DOM在每次鼠标移动时:将boundingRect缓存在某个var/属性的左侧和顶部。

如果 您的页面允许滚动,请不要忘记处理"滚动"事件以及在滚动时重新计算边界矩形。

坐标以css像素提供:如果使用css缩放画布,确保其边界为0,并使用offsetWidth和offsetHeight计算正确位置由于您还希望缓存这些性能值,并避免过多的全局变量,因此代码将如下所示:

var mouse = { x:0, y:0, down:false };
function setupMouse() {
    var rect = cv.getBoundingClientRect();
    var rectLeft = rect.left;
    var rectTop = rect.top;
    var cssScaleX = cv.width / cv.offsetWidth;
    var cssScaleY = cv.height / cv.offsetHeight;
    function handleMouseEvent(e) {
        mouse.x = (e.clientX - rectLeft) * cssScaleX;
        mouse.y = (e.clientY - rectTop) * cssScaleY;
    }
    window.addEventListener('mousedown', function (e) {
        mouse.down = true;
        handleMouseEvent(e);
    });
    window.addEventListener('mouseup', function (e) {
        mouse.down = false;
        handleMouseEvent(e);
    });
    window.addEventListener('mouseout', function (e) {
        mouse.down = false;
        handleMouseEvent(e);
    });
    window.addEventListener('mousemove',  handleMouseEvent );
};

最后一句话:至少可以说,对事件处理程序进行性能测试是有问题的,除非您能够确保在每次测试中执行相同的移动/单击。没有办法比上面的代码更快地处理事情了。好吧,如果你确定画布没有经过css缩放,你可能会节省2个mul,但无论如何,到目前为止,用于输入处理的浏览器开销太大了,不会改变任何事情。

我建议使用getBoundingClientRect()

当浏览器进行重新流动/更新时,此方法将返回图元的位置(相对于视图端口)。

它被广泛支持跨浏览器,因此没有理由不使用它。不过,如果你需要向后兼容性来支持旧浏览器,你应该使用不同的方法。

然而,使用这种方法时需要注意以下几点:

  • 如果>0,元素CSS填充会影响相对于画布的位置
  • 如果>0,元素CSS边框宽度会影响相对于画布的位置
  • 生成的对象是静态的,即即使在使用对象之前更改了视图端口,也不会更新

您需要手动将顶部和左侧的宽度添加到您的位置。这些都包含在方法中,这意味着您需要对此进行补偿,以获得相对于画布的位置。

如果你不使用边框和/或填充,这很简单。但是,当你需要添加这些像素的绝对宽度时,或者如果它们是未知的或动态的,你需要使用getComputedStyle方法和getPropertyValue来获得它们(即使边界/填充的原始定义是不同的单位,这些方法也总是以像素为单位)。

如果使用的话,这些值可以缓存大部分,除非边框和填充也在更改,但在大多数用例中情况并非如此。

你澄清了性能不是问题所在,你这样做是正确的,因为你列出的这些方法都不是真正的瓶颈(但如果你知道如何进行性能测试,当然完全可以衡量)。您使用的方法本质上是个人品味,而不是性能问题,因为瓶颈在于推动事件本身通过事件链。

但最"现代"的(如果我们将现代定义为更新和更方便的)是getBoundingClientRect(),避免元素上的边框/填充使其易于使用。