不同于HTML或SVG标签可以直接绑定事件,Canvas是使用JavaScript来绘制内容,这意味着其内容没有具体的DOM,所以Canvas渲染引擎都会自己实现一套事件机制。Konva的事件机制支持图形的选中、拖拽等交互处理,同时还支持单个图形对象绑定对应的事件。Konva版本是v9.2.1。
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(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节点接收事件触发相关事件处理程序运行,相关的事件处理程序的逻辑简单概括就是如下两点:
通过上面的机制从而实现事件冒泡,并且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实际上是在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对应的实例方法
Konva是通过在Layer实例化时增加一个HitCanvas来服务于后续图形命中逻辑,当绘制图形时会在SceneCanvas以及HitCanvas都绘制,只不过HitCanvas的图形绘制都会使用唯一色值填充,并将这个颜色值保存到集合中,这种方案可以很准确的处理复杂图形的选中,但是也存在相应的问题:
实际上目前对于Canvas图形拾取策略除了Konva这种色值法方案,还有几何计算法,具体的优缺点比较可以查看这篇文章
Canvas 的拾取方案选择。