前言
不同于HTML或SVG标签可以直接绑定事件,Canvas是使用JavaScript来绘制内容,这意味着其内容没有具体的DOM,所以Canvas渲染引擎都会自己实现一套事件机制。Konva的事件机制支持图形的选中、拖拽等交互处理,同时还支持单个图形对象绑定对应的事件。Konva版本是v9.2.1。
Konva事件机制
Konva支持绑定的事件包括Mouse类事件、Touch类事件、Drag事件、Transform事件,具体事件可查看官网说明。使用下面的实例来说明Konva的事件机制:
const stage = new Konva.Stage({
container: 'root',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const circle = new Konva.Circle({
x: 230,
y: 100,
radius: 60,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
circle.on('click', () => {
console.log('click circle');
});
stage.on('click', () => {
console.log('click stage')
})
layer.add(circle)
stage.add(layer)
上面案例实现对Circle以及Stage绑定点击事件,在Konva中事件实例方法都是Node基类提供的,相关方法如下:
- on/addEventListener:注册事件
- off/removeEventListener:解绑事件
- fire/dispatchEvent:触发事件
主要关注事件的注册方法的逻辑,主要的处理逻辑如下:
on(evtStr, handler) {
...
if (!this.eventListeners[baseEvent]) {
this.eventListeners[baseEvent] = [];
}
this.eventListeners[baseEvent].push({
name: name,
handler: handler,
});
}
每个继承自Node基类的容器类以及图形类的实例对象都使用eventListeners属性来保存事件以及处理程序,故而每个图形元素都可以注册事件。注册完成后如何触发呢?除了在代码层次调用fire触发事件的方式,就是界面交互触发。
事件响应
在之前Konva基本使用文章中实际上可知Stage类会构建内容节点并且作为事件接收层,content节点绑定的事件以及对应处理程序如下:
[
[MOUSEENTER, '_pointerenter'],
[MOUSEDOWN, '_pointerdown'],
[MOUSEMOVE, '_pointermove'],
[MOUSEUP, '_pointerup'],
[MOUSELEAVE, '_pointerleave'],
[TOUCHSTART, '_pointerdown'],
[TOUCHMOVE, '_pointermove'],
[TOUCHEND, '_pointerup'],
[TOUCHCANCEL, '_pointercancel'],
[MOUSEOVER, '_pointerover'],
[WHEEL, '_wheel'],
[CONTEXTMENU, '_contextmenu'],
[POINTERDOWN, '_pointerdown'],
[POINTERMOVE, '_pointermove'],
[POINTERUP, '_pointerup'],
[POINTERCANCEL, '_pointercancel'],
[LOSTPOINTERCAPTURE, '_lostpointercapture']
]
click事件会触发_pointerdown、_pointerup,主要的处理逻辑总结如下:
从Stage Content DOM节点接收事件触发相关事件处理程序运行,相关的事件处理程序的逻辑简单概括就是如下两点:
- 计算当前点击位置的相对Content节点的位置坐标:先计算Content DOM节点的偏移位置数据,相对Content的位置坐标 = 当前点击位置的屏幕坐标 - Content偏移位置
- 根据位置坐标然后根据相关逻辑得到当前对应的Shape对象
- 触发Shape的fireAndBubble实例方法,该方法内部会实现事件冒泡,即查找当前Shape是否存在父节点,只要存在父节点就会一直调用fire实例方法触发事件事件
通过上面的机制从而实现事件冒泡,并且Konva还提供listening属性来实现对应图形对象是否触发事件。
图形命中策略
在上小结事件响应中根据位置坐标找到对应的Shape对象这个逻辑并没有细说,实际上这部分逻辑非常重要,这里细细说明。实际上这部分的逻辑是在getIntersection实例方法中,该实例方法的核心逻辑如下图所示:
当根据位置选中图形对象就会遍历所有的Layers进行处理,最后调用的核心逻辑如下:
_getIntersection(pos) {
const ratio = this.hitCanvas.pixelRatio;
const p = this.hitCanvas.context.getImageData(
Math.round(pos.x * ratio),
Math.round(pos.y * ratio),
1, 1
).data;
const p3 = p[3];
// fully opaque pixel
if (p3 === 255) {
const colorKey = Util._rgbToHex(p[0], p[1], p[2]);
const shape = shapes[HASH + colorKey];
if (shape) {
return {
shape: shape,
};
}
}
...
}
从上面逻辑可以看出两点核心处理:
- 会调用hitCanvas的getImageData,获取对应位置的图像像素值
- 根据像素值查找shapes中对应的shape对象
hitCanvas实际上是在Layer构建实例时调用HitCanvas创建的Canvas层,而shapes的处理逻辑是对应Shape基类初始化时的逻辑,具体如下:
class Shape extends Node {
constructor(config) {
super(config);
// set colorKey
let key;
while (true) {
key = Util.getRandomColor();
if (key && !(key in shapes)) {
break;
}
}
this.colorKey = key;
shapes[key] = this;
}
...
}
所有继承自Shape的图形类都会执行上面逻辑,其会生成唯一的颜色值并且保存到shapes map中,而hitCanvas在SceneCanvas绘制对应图形的也会绘制相同的图形,只不过hitCanvas绘制的图形被填充的颜色是单一颜色,通过色值可以快速准确的定位对应坐标位置的图形对象。
总结
Konva实现的事件机制可以实现图形对象绑定事件,实际上所有继承自Node基类的类,都可以使用on等实例方法来监听对应事件。
整个的事件机制总结如下:
-
在Stage实例化时生成使用Content节点,并且绑定相关事件到该节点上,对应事件处理程序就是Stage上相关实例方法
-
当界面交互触发对应事件后,就会调用Stage对应的实例方法
- 首先是根据鼠标点击位置的屏幕坐标以及Content节点位置计算出相对于Content的位置坐标
- 然后使用getImageData得到位于内存中HitCanvas相应位置坐标下颜色值,根据颜色值在集合中查找是否有对应的Shape对象
- 最后根据父子关系链递归触发事件,从而执行用户自定义的事件绑定处理程序,实现事件冒泡
Konva是通过在Layer实例化时增加一个HitCanvas来服务于后续图形命中逻辑,当绘制图形时会在SceneCanvas以及HitCanvas都绘制,只不过HitCanvas的图形绘制都会使用唯一色值填充,并将这个颜色值保存到集合中,这种方案可以很准确的处理复杂图形的选中,但是也存在相应的问题:
- 额外增加HitCanvas绘制图形,增大内存使用,并且每次图形更新都要额外处理HitCanvas
- 颜色值作为唯一键值,其大小是有限的,即255 * 255 * 255,当然这个数量级页面本身也会存在性能问题了
实际上目前对于Canvas图形拾取策略除了Konva这种色值法方案,还有几何计算法,具体的优缺点比较可以查看这篇文章
Canvas 的拾取方案选择。