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

IT技术 javascript canvas mouse
2021-03-05 13:00:35

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

方法一:

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

方法二:

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

方法三:

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

方法四:

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;

等等。

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

3个回答

这似乎有效。我想这基本上就是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。

将鼠标移到蓝色区域上,它将在其下方绘制一个像素。

那么,最好的建议是?, 除非您想完成所有这些步骤,否则始终将画布的边框和内边距设为 0。如果边界和填充为零你可以canvas.clientWidthcanvas.clientHeightcontentDisplayWidthcontentDisplayHeight在下面的例子中,所有的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;  
}

您以画布为目标,因此您仅以最近的浏览器为目标。
所以你可以忘记方法 4 的 pageX 内容。
方法 1 在嵌套画布的情况下失败。
方法 3 与方法 2 类似,但由于您手动完成,因此速度较慢。

-->> 要走的路是选项2。

现在,由于您担心性能,您不想在每次鼠标移动时调用 DOM:将 boundingRect left 和 top 缓存在某个 var/property 中。

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

坐标以 css 像素提供:如果您使用 css 缩放 Canvas,请确保其边框为 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 muls,但无论如何,到目前为止,浏览器输入处理的开销太大了,它不会改变任何事情。

有关此答案不起作用的情况,请参见下文
2021-04-20 13:00:35
别客气。缓存边界矩形(+滚动更新)是否值得付出一点努力,因为 DOM 访问速度非常慢。(除非可能在项目的早期部分)。
2021-04-22 13:00:35
谢谢。似乎getBoundingClientRect()是那时要走的路。我并不真正担心性能,因为我想知道哪种方法最现代和最适用。
2021-05-01 13:00:35

我会推荐使用getBoundingClientRect().

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

它被广泛支持跨浏览器,所以真的没有理由使用它 IMO。但是,如果您需要向后兼容以支持旧浏览器,您应该使用不同的方法。

但是,在使用此方法时,您需要注意以下几点:

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

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

如果您不使用边框和/或填充,那很简单。但是,当您这样做时,您要么需要添加以像素为单位的绝对宽度,或者如果它们是未知的或动态的,您将需要弄乱getComputedStyle方法并getPropertyValue获取它们(这些总是以像素为单位给出大小,即使原始定义边框/填充位于不同的单元中)。

如果使用这些值,您可以在大部分情况下缓存这些值,除非边框和填充也发生变化,但在大多数用例中并非如此。

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

但最“现代”(如果我们将现代定义为更新和更方便)是getBoundingClientRect()避免元素上的边框/填充使其使用起来轻而易举。