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

时间:2013-11-19 00:16:36

标签: javascript canvas mouse

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

方法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;

等等。

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

3 个答案:

答案 0 :(得分: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,无论其拉伸的大小如何。

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

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。如果边框和填充为零,则下面的示例中canvas.clientWidthcanvas.clientHeight只有contentDisplayWidthcontentDisplayHeight,所有nonContextXXX值都会变为0。 / p>

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>

答案 1 :(得分:2)

您定位画布,因此您只定位最近的浏览器 所以你可以忘记方法4的pageX内容。
在嵌套画布的情况下,方法1失败 方法3就像方法2一样,但是因为你手工完成它会慢一些。

- &GT;&GT;方法是选择2.

既然你担心表演,你不想打电话给DOM 在每个鼠标移动中:在一些var / property中缓存boundingRect left和top。

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

坐标以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,但无论如何,截至目前,输入处理的浏览器开销太大,以至于它不会改变任何东西。

答案 2 :(得分:1)

我建议使用getBoundingClientRect()

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

它得到了跨浏览器的广泛支持,因此没有理由使用它IMO。但是,如果您需要向后兼容性以支持旧浏览器,则应使用其他方法。

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

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

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

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

除非边框和填充也在变化,否则这些值可以在大多数情况下缓存,除非边框和填充也在变化,但在大多数用例中情况并非如此。

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

但最“现代”(如果我们将现代定义为更新且更方便)是getBoundingClientRect()并且避免元素上的边框/填充使得它变得轻而易举。