如何给canvas元素添加一个简单的onClick事件处理程序?

172

我是一名有经验的Java程序员,但第一次接触大约十年前的JavaScript/HTML5。我完全被应该是最简单的事情难住了。

例如,我只想画点东西并在其上添加一个事件处理程序。我确定我正在做一些愚蠢的事情,但我已经搜索了所有建议的内容(例如,这个问题的答案:Add onclick property to input with JavaScript),但没有任何作用。我正在使用Firefox 10.0.1。我的代码如下。您会看到几行注释,在每行的末尾都有一个关于发生什么或不发生什么的描述。

正确的语法是什么?我快疯了!

<html>
<body>
    <canvas id="myCanvas" width="300" height="150"/>
    <script language="JavaScript">
        var elem = document.getElementById('myCanvas');
        // elem.onClick = alert("hello world");  - displays alert without clicking
        // elem.onClick = alert('hello world');  - displays alert without clicking
        // elem.onClick = "alert('hello world!')";  - does nothing, even with clicking
        // elem.onClick = function() { alert('hello world!'); };  - does nothing
        // elem.onClick = function() { alert("hello world!"); };  - does nothing
        var context = elem.getContext('2d');
        context.fillStyle = '#05EFFF';
        context.fillRect(0, 0, 150, 100);
    </script>

</body>


12
请使用 onclick 替代 onClick - noob
2
详细说明为什么这些尝试没有成功...前几个注释会立即显示一个警报,因为您直接在<script>中调用了alert(),而不是定义一个将调用alert()的函数。其余的都没有做任何事情,因为onclick的大小写不正确。 - LarsH
你可以使用这个库 https://jsfiddle.net/user/zlatnaspirala/fiddles/ ,查看 https://bitbucket.org/nikola_l/visual-js/ 。你将获得许多功能+。 - Nikola Lukic
8个回答

278

在绘制 canvas 元素时,实际上是在以即时模式绘制位图。

绘制的元素(形状、线条、图像)除了使用的像素和颜色外,没有其他表示。

因此,要在 canvas 元素(形状)上获得点击事件,您需要在 canvas HTML元素上捕获点击事件,并使用一些数学来确定哪个元素被点击,前提是您正在存储元素的宽度/高度和 x/y 偏移量。

要向您的 canvas 元素添加click事件,请使用...

canvas.addEventListener('click', function() { }, false);

为了确定被点击的元素...

var elem = document.getElementById('myCanvas'),
    elemLeft = elem.offsetLeft + elem.clientLeft,
    elemTop = elem.offsetTop + elem.clientTop,
    context = elem.getContext('2d'),
    elements = [];

// Add event listener for `click` events.
elem.addEventListener('click', function(event) {
    var x = event.pageX - elemLeft,
        y = event.pageY - elemTop;

    // Collision detection between clicked offset and element.
    elements.forEach(function(element) {
        if (y > element.top && y < element.top + element.height 
            && x > element.left && x < element.left + element.width) {
            alert('clicked an element');
        }
    });

}, false);

// Add element.
elements.push({
    colour: '#05EFFF',
    width: 150,
    height: 100,
    top: 20,
    left: 15
});

// Render elements.
elements.forEach(function(element) {
    context.fillStyle = element.colour;
    context.fillRect(element.left, element.top, element.width, element.height);
});​

jsFiddle

这段代码将一个 click 事件附加到 canvas 元素,然后将一个形状(在我的代码中称为 element)推入一个 elements 数组中。您可以在此添加尽可能多的形状。

创建对象数组的目的是为了稍后可以查询它们的属性。在所有元素都被推入数组之后,我们循环遍历每个元素并基于它们的属性呈现每个元素。

当触发 click 事件时,代码循环遍历元素并确定单击是否落在 elements 数组中的任何一个元素上。如果是,则触发一个 alert(),您可以轻松修改它以执行诸如删除该数组项之类的操作,在这种情况下,您需要一个单独的 render 函数来更新 canvas


为了完整起见,以下是为什么您的尝试未奏效的原因...

elem.onClick = alert("hello world"); // displays alert without clicking

这是将alert()的返回值分配给elemonClick属性。它立即调用了alert()

elem.onClick = alert('hello world');  // displays alert without clicking

在JavaScript中,'"在语义上是相同的,词法分析器可能使用['"]来表示引号。

elem.onClick = "alert('hello world!')"; // does nothing, even with clicking

您正在将一个字符串分配给elemonClick属性。

elem.onClick = function() { alert('hello world!'); }; // does nothing

JavaScript 区分大小写。 onclick 属性是陈旧的附加事件处理程序的方法。它仅允许使用该属性附加一个事件,并且在序列化 HTML 时可能会丢失该事件。

elem.onClick = function() { alert("hello world!"); }; // does nothing

再次提醒,' === "


1
非常感谢,尽管我对最后一个不是完全清楚。我正在遵循这里的示例:https://developer.mozilla.org/en/DOM/element.onclick(使用他们描述的匿名函数),它可以工作,即使我将span更改为canvas。因此,我可以慢慢将他们的示例转换为我的示例,以查看我做错了什么。感谢您详细的回复!解释为什么我的各种尝试是错误的尤其有帮助。 - Jer
1
@Jer 不用担心,如果你有更多问题,请随时在 Twitter 上发布并 @ 我,@alexdickson - alex
1
@HashRocketSyntax 这应该没问题,只需更新内存中对象的位置,并使用这些定义来渲染即可。 - alex
@Kalanos 是的,你说得对,如果画布移动了,比如它在页面中心并且用户调整了窗口大小,这个方法就不起作用了。为了解决这个问题,你可以将 elemLeftelemTop 的定义移到事件监听器内部。 - Zevgon
这段代码如何更改以便检测旋转矩形上的鼠标点击? - Jack Stein
显示剩余5条评论

44

2021年:

要在HTML5画布中创建一个可跟踪的元素,您应该使用new Path2D()方法。

首先,在您的画布上侦听触摸或鼠标事件(例如onclickondblclickoncontextmenuonmousemove等),以获取点坐标event.offsetXevent.offsetY,然后使用CanvasRenderingContext2D.isPointInPath()CanvasRenderingContext2D.isPointInStroke()来精确检查鼠标是否悬停在该事件中的元素上。

IsPointInPath:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// Create circle
const circle = new Path2D();
circle.arc(150, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(circle);

// Listen for mouse moves
canvas.addEventListener('mousemove', function(event) {
  // Check whether point is inside circle
  if (ctx.isPointInPath(circle, event.offsetX, event.offsetY)) {
    ctx.fillStyle = 'green';
  }
  else {
    ctx.fillStyle = 'red';
  }

  // Draw circle
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fill(circle);
});
<canvas id="canvas"></canvas>

IsPointInStroke:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// Create ellipse
const ellipse = new Path2D();
ellipse.ellipse(150, 75, 40, 60, Math.PI * .25, 0, 2 * Math.PI);
ctx.lineWidth = 25;
ctx.strokeStyle = 'red';
ctx.fill(ellipse);
ctx.stroke(ellipse);

// Listen for mouse moves
canvas.addEventListener('mousemove', function(event) {
  // Check whether point is inside ellipse's stroke
  if (ctx.isPointInStroke(ellipse, event.offsetX, event.offsetY)) {
    ctx.strokeStyle = 'green';
  }
  else {
    ctx.strokeStyle = 'red';
  }

  // Draw ellipse
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fill(ellipse);
  ctx.stroke(ellipse);
});
<canvas id="canvas"></canvas>

多个元素的示例:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

const circle = new Path2D();
circle.arc(50, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(circle);

const circletwo = new Path2D();
circletwo.arc(200, 75, 50, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(circletwo);

// Listen for mouse moves
canvas.addEventListener('mousemove', function(event) {
  // Check whether point is inside circle
  if (ctx.isPointInPath(circle, event.offsetX, event.offsetY)) {
    ctx.fillStyle = 'green';
    ctx.fill(circle);
  }
  else {
    ctx.fillStyle = 'red';
    ctx.fill(circle);
  }
  
    if (ctx.isPointInPath(circletwo, event.offsetX, event.offsetY)) {
    ctx.fillStyle = 'blue';
    ctx.fill(circletwo);
  }
  else {
    ctx.fillStyle = 'red';
    ctx.fill(circletwo);
  }
  
});
html {cursor: crosshair;}
<canvas id="canvas"></canvas>

如果你有一个需要检查的动态元素列表,你可以使用循环来逐个检查它们,就像这样:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
var elementslist = []

const circle = new Path2D();
circle.arc(50, 75, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(circle);

const circletwo = new Path2D();
circletwo.arc(150, 75, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(circletwo);

const circlethree = new Path2D();
circlethree.arc(250, 75, 30, 0, 2 * Math.PI);
ctx.fillStyle = 'red';
ctx.fill(circlethree);

elementslist.push(circle,circletwo,circlethree)

document.getElementById("canvas").addEventListener('mousemove', function(event) {
event = event || window.event;
var ctx = document.getElementById("canvas").getContext("2d")

for (var i = elementslist.length - 1; i >= 0; i--){  

if (elementslist[i] && ctx.isPointInPath(elementslist[i], event.offsetX, event.offsetY)) {
document.getElementById("canvas").style.cursor = 'pointer';
    ctx.fillStyle = 'orange';
    ctx.fill(elementslist[i]);
return
} else {
document.getElementById("canvas").style.cursor = 'default';
    ctx.fillStyle = 'red';
    for (var d = elementslist.length - 1; d >= 0; d--){ 
    ctx.fill(elementslist[d]);
    }
}
}  

});
<canvas id="canvas"></canvas>


来源:


4
作为对Alex答案的替代方案:
您可以使用SVG绘图而不是Canvas绘图。在那里,您可以直接向绘制的DOM对象添加事件。
例如,请参见:
使svg图像对象可通过onclick点击,避免使用绝对定位

1
抱歉我的热情有些过于高涨,只是我遇到了一个使用SVG是显而易见的解决方案的问题,直到看到你的评论之前我没有想到。^^ - Elias Dorneles

3

2
"[addHitRegion]已经过时。虽然它在某些浏览器中仍然可以使用,但由于它可能随时被删除,因此不建议使用它。请尽量避免使用它。" - 从您提供的dev.moz链接中翻译,以便为其他人节省点击时间。 - Chris

2

也许回答有点晚了,但我在准备我的70-480考试时读到这篇文章,并发现这很有效 -

var elem = document.getElementById('myCanvas');
elem.onclick = function() { alert("hello world"); }

注意事件使用 onclick 而不是 onClick

JS Bin 示例。


2
OP想要在画布内的一个元素上添加onclick,而不是画布本身。 - Systems Rebooter

1
你还可以将DOM元素,如div放置在画布上方,代表你的画布元素并以相同方式定位。
现在,你可以将事件监听器附加到这些
并运行必要的操作。

对于不太懂 iOS 编程的程序员来说,学会如何做这件事会非常棒 :) - Fattie

1
作为另一种相对静态画布的廉价替代方案,使用具有usemap定义的覆盖img元素是快速而简单的。在基于多边形的画布元素(如饼图)上特别有效。

0

Alex Answer非常不错,但在使用上下文旋转时,可能很难追踪x、y坐标,因此我制作了一个演示,展示如何跟踪它们。

基本上,我正在使用这个函数,并给出角度和绘制对象之前行进的距离。

function rotCor(angle, length){
    var cos = Math.cos(angle);
    var sin = Math.sin(angle);

    var newx = length*cos;
    var newy = length*sin;

    return {
        x : newx,
        y : newy
    };
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接