向canvas元素添加一个单击事件处理程序,返回单击的x和y坐标(相对于canvas元素),最简单的方法是什么?

不需要传统浏览器兼容性,Safari、Opera和Firefox就可以了。


当前回答

我做了一个完整的演示,可以在每个浏览器中使用这个问题的解决方案的完整源代码:在Javascript中单击Canvas的鼠标坐标。要尝试演示,复制代码并将其粘贴到文本编辑器中。然后将其保存为example.html,最后用浏览器打开该文件。

其他回答

这里是一个简化的解决方案(这并不适用于边框/滚动):

function click(event) {
    const bound = event.target.getBoundingClientRect();

    const xMult = bound.width / can.width;
    const yMult = bound.height / can.height;

    return {
        x: Math.floor(event.offsetX / xMult),
        y: Math.floor(event.offsetY / yMult),
    };
}

所以这是一个简单但比看起来更复杂的话题。

首先,这里通常有一些合并的问题

如何获得元素相对鼠标坐标 如何获得画布像素鼠标坐标为2D画布API或WebGL

所以,答案

如何获得元素相对鼠标坐标

无论元素是否是画布,获取元素相对鼠标坐标对所有元素都是相同的。

“如何获得画布相对鼠标坐标”这个问题有两个简单的答案

简单答案#1使用offsetX和offsetY

canvas.addEventListner('mousemove', (e) => {
  const x = e.offsetX;
  const y = e.offsetY;
});

这个答案适用于Chrome, Firefox和Safari。与所有其他事件值不同,offsetX和offsetY将CSS转换考虑在内。

offsetX和offsetY最大的问题是,截至2019/05,它们在触摸事件上不存在,因此不能与iOS Safari一起使用。它们确实存在于指针事件,存在于Chrome和Firefox中,但不包括Safari,尽管显然Safari正在处理它。

另一个问题是事件必须在画布本身上。如果你把它们放在其他元素或窗口上,你以后就不能选择画布作为你的参考点了。

简单的答案#2使用clientX, clienti和canvas.getBoundingClientRect

如果你不关心CSS转换,下一个最简单的答案是调用canvas。getBoundingClientRect()并从clientX中减去左边,从cliententy中减去顶部

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
});

只要没有CSS转换,这就可以工作。它也适用于触摸事件,因此也适用于Safari iOS

canvas.addEventListener('touchmove', (e) => {
  const rect = canvas. getBoundingClientRect();
  const x = e.touches[0].clientX - rect.left;
  const y = e.touches[0].clientY - rect.top;
});

如何获得画布像素鼠标坐标为2D画布API

为此,我们需要将上面得到的值从画布显示的大小转换为画布本身的像素数

与画布。getBoundingClientRect和clientX和clientY

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  const elementRelativeX = e.clientX - rect.left;
  const elementRelativeY = e.clientY - rect.top;
  const canvasRelativeX = elementRelativeX * canvas.width / rect.width;
  const canvasRelativeY = elementRelativeY * canvas.height / rect.height;
});

或使用offsetX和offsetY

canvas.addEventListener('mousemove', (e) => {
  const elementRelativeX = e.offsetX;
  const elementRelativeY = e.offsetY;
  const canvasRelativeX = elementRelativeX * canvas.width / canvas.clientWidth;
  const canvasRelativeY = elementRelativeY * canvas.height / canvas.clientHeight;
});

注意:在所有情况下都不要给画布添加填充或边框。这样做将极大地复杂化代码。而不是你想要一个边框或填充在一些其他元素的画布周围,并添加填充和或边框到外部元素。

使用事件的工作示例。offsetX, event.offsetY

[...document.querySelectorAll('canvas')].forEach((canvas) => { const ctx = canvas.getContext('2d'); ctx.canvas.width = ctx.canvas.clientWidth; ctx.canvas.height = ctx.canvas.clientHeight; let count = 0; function draw(e, radius = 1) { const pos = { x: e.offsetX * canvas.width / canvas.clientWidth, y: e.offsetY * canvas.height / canvas.clientHeight, }; document.querySelector('#debug').textContent = count; ctx.beginPath(); ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5); ctx.fill(); } function preventDefault(e) { e.preventDefault(); } if (window.PointerEvent) { canvas.addEventListener('pointermove', (e) => { draw(e, Math.max(Math.max(e.width, e.height) / 2, 1)); }); canvas.addEventListener('touchstart', preventDefault, {passive: false}); canvas.addEventListener('touchmove', preventDefault, {passive: false}); } else { canvas.addEventListener('mousemove', draw); canvas.addEventListener('mousedown', preventDefault); } }); function hsl(h, s, l) { return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`; } .scene { width: 200px; height: 200px; perspective: 600px; } .cube { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; animation-duration: 16s; animation-name: rotate; animation-iteration-count: infinite; animation-timing-function: linear; } @keyframes rotate { from { transform: translateZ(-100px) rotateX( 0deg) rotateY( 0deg); } to { transform: translateZ(-100px) rotateX(360deg) rotateY(720deg); } } .cube__face { position: absolute; width: 200px; height: 200px; display: block; } .cube__face--front { background: rgba(255, 0, 0, 0.2); transform: rotateY( 0deg) translateZ(100px); } .cube__face--right { background: rgba(0, 255, 0, 0.2); transform: rotateY( 90deg) translateZ(100px); } .cube__face--back { background: rgba(0, 0, 255, 0.2); transform: rotateY(180deg) translateZ(100px); } .cube__face--left { background: rgba(255, 255, 0, 0.2); transform: rotateY(-90deg) translateZ(100px); } .cube__face--top { background: rgba(0, 255, 255, 0.2); transform: rotateX( 90deg) translateZ(100px); } .cube__face--bottom { background: rgba(255, 0, 255, 0.2); transform: rotateX(-90deg) translateZ(100px); } <div class="scene"> <div class="cube"> <canvas class="cube__face cube__face--front"></canvas> <canvas class="cube__face cube__face--back"></canvas> <canvas class="cube__face cube__face--right"></canvas> <canvas class="cube__face cube__face--left"></canvas> <canvas class="cube__face cube__face--top"></canvas> <canvas class="cube__face cube__face--bottom"></canvas> </div> </div> <pre id="debug"></pre>

使用画布的工作示例。getBoundingClientRect和事件。clientX和event.clientY

const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); ctx.canvas.width = ctx.canvas.clientWidth; ctx.canvas.height = ctx.canvas.clientHeight; let count = 0; function draw(e, radius = 1) { const rect = canvas.getBoundingClientRect(); const pos = { x: (e.clientX - rect.left) * canvas.width / canvas.clientWidth, y: (e.clientY - rect.top) * canvas.height / canvas.clientHeight, }; ctx.beginPath(); ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2); ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5); ctx.fill(); } function preventDefault(e) { e.preventDefault(); } if (window.PointerEvent) { canvas.addEventListener('pointermove', (e) => { draw(e, Math.max(Math.max(e.width, e.height) / 2, 1)); }); canvas.addEventListener('touchstart', preventDefault, {passive: false}); canvas.addEventListener('touchmove', preventDefault, {passive: false}); } else { canvas.addEventListener('mousemove', draw); canvas.addEventListener('mousedown', preventDefault); } function hsl(h, s, l) { return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`; } canvas { background: #FED; } <canvas width="400" height="100" style="width: 300px; height: 200px"></canvas> <div>canvas deliberately has differnt CSS size vs drawingbuffer size</div>

首先,正如其他人所说,您需要一个函数来获取canvas元素的位置。这里有一个方法,它比本页上的其他一些方法更优雅。你可以将任何元素传递给它,并获得它在文档中的位置:

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

现在计算游标相对于它的当前位置:

$('#canvas').mousemove(function(e) {
    var pos = findPos(this);
    var x = e.pageX - pos.x;
    var y = e.pageY - pos.y;
    var coordinateDisplay = "x=" + x + ", y=" + y;
    writeCoordinateDisplay(coordinateDisplay);
});

注意,我将泛型findPos函数与事件处理代码分开。(本来就应该如此。我们应该尽量让每个函数只执行一个任务。)

offsetLeft和offsetTop的值相对于offsetParent,它可以是某个包装器div节点(或者其他任何东西)。当没有元素包裹画布时,它们是相对于主体的,因此没有要减去的偏移量。这就是为什么我们需要在做其他事情之前确定画布的位置。

类似地,e.pageX和e.pageY给出了光标相对于文档的位置。这就是为什么我们从这些值中减去画布的偏移量来达到真实位置。

定位元素的另一种选择是直接使用e.layerX和e.layerY的值。这种方法不如上面的方法可靠,有两个原因:

当事件不在定位的元素中发生时,这些值也相对于整个文档 它们不是任何标准的一部分

三JS r77

var x = event.offsetX == undefined ? event.layerX : event.offsetX;
var y = event.offsetY == undefined ? event.layerY : event.offsetY;

mouse2D.x = ( x / renderer.domElement.width ) * 2 - 1;
mouse2D.y = - ( y / renderer.domElement.height ) * 2 + 1;

在尝试了许多解决方案后。这对我很管用。可能会帮助其他人,因此发帖。从这里开始

下面是对Ryan Artecona关于画布宽度可变(%)的答案的一个小修改:

 HTMLCanvasElement.prototype.relMouseCoords = function (event) {
    var totalOffsetX = 0;
    var totalOffsetY = 0;
    var canvasX = 0;
    var canvasY = 0;
    var currentElement = this;

    do {
        totalOffsetX += currentElement.offsetLeft;
        totalOffsetY += currentElement.offsetTop;
    }
    while (currentElement = currentElement.offsetParent)

    canvasX = event.pageX - totalOffsetX;
    canvasY = event.pageY - totalOffsetY;

    // Fix for variable canvas width
    canvasX = Math.round( canvasX * (this.width / this.offsetWidth) );
    canvasY = Math.round( canvasY * (this.height / this.offsetHeight) );

    return {x:canvasX, y:canvasY}
}