Самый современный метод получения позиции мыши в холсте в родном JavaScript

Во-первых, я знаю, что этот вопрос задавался много раз. Однако предоставленные ответы несовместимы, и для получения позиции мыши используются различные методы. Несколько примеров:

Способ 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;

и т.д.

Что мне любопытно, какой метод является самым современным с точки зрения поддержки браузера и удобства в том, чтобы получить положение мыши на холсте. Или это те вещи, которые имеют незначительное влияние, и любой из вышеперечисленных является хорошим выбором? (Да, я понимаю, что коды выше не совсем одинаковы)

Ответ 1

Вы нацеливаете canvas, поэтому вы нацеливаете только недавние браузеры.
Таким образом, вы можете забыть о материале pageX метода 4.
Метод 1 терпит неудачу в случае вложенного холста.
Метод 3 подобен методу 2, но медленнее, так как вы делаете это вручную.

- → Способ перехода - это вариант 2.

Теперь, когда вы беспокоитесь о выступлениях, вы не хотите звонить в DOM на каждом перемещении мыши: кешируйте boundingRect слева и сверху внутри некоторого свойства var/.

Если ваша страница позволяет прокручивать, не забудьте обработать событие 'scroll' и повторно вычислить ограничивающий прямоугольник на прокрутке.

Координаты представлены в пикселях css: Если, вы масштабируете Canvas с помощью 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 );
};

Последнее слово: тестирование производительности обработчиком событий является, мягко говоря, сомнительным, если вы не можете гарантировать, что одни и те же ходы/клики выполняются во время каждого теста. Нет способа обрабатывать вещи быстрее, чем в приведенном выше коде. Ну, вы можете сохранить 2 muls, если вы уверены, что холст не масштабируется css, но в любом случае на данный момент накладные расходы браузера для обработки ввода настолько велики, что он ничего не изменит.

Ответ 2

Я бы рекомендовал использовать getBoundingClientRect().

Когда браузер выполняет повторный поток/обновление, этот метод возвращает позицию (относительно вида-порта) элемента.

Он широко поддерживает кросс-браузер, поэтому нет причин не использовать его IMO. Хотя, если вам нужна обратная совместимость для поддержки старых браузеров, вы должны использовать другой метод.

Однако при использовании этого метода вам нужно знать несколько вещей:

  • Элемент CSS padding влияет на положение относительно холста, если > 0.
  • Ширина границы элемента Element влияет на позицию относительно холста, если > 0.
  • Получаемый объект является статическим, т.е. он не обновляется, даже если, например, порт просмотра изменился до использования объекта.

Вам нужно добавить ширину этих, сверху и слева, в свою позицию вручную. Они включены в метод, который означает, что вам нужно компенсировать это, чтобы позиция была относительно холста.

Если вы не используете границу и/или дополняете ее просто. Но когда вам нужно либо добавить абсолютную ширину в пикселях, либо если они неизвестны или динамичны, вам нужно будет использовать метод getComputedStyle и getPropertyValue, чтобы получить их (они дают размер всегда в пикселях даже если исходное определение границы/заполнения было в другом блоке).

Это значения, которые вы можете кэшировать по большей части, если они используются, если границы и дополнения не изменяются, но в большинстве случаев использования это не так.

Вы уточнили, что производительность не является проблемой, и вы правы в этом, так как ни один из этих методов, которые вы перечисляете, не является реальным узким местом (но, конечно, вполне можно измерить, если вы знаете, как выполнять тестирование производительности). Какой метод вы используете, по сути, является личным вкусом, а не проблемой производительности, поскольку узкое место лежит в том, что он сам продвигает события через цепочку событий.

Но самый "современный" (если мы определяем современный как более новый и более удобный) - это getBoundingClientRect() и избегание границы/дополнения на элементе делает его удобным.

Ответ 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>