Konva事件机制

前言

不同于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,主要的处理逻辑总结如下:
Konva事件机制_第1张图片
从Stage Content DOM节点接收事件触发相关事件处理程序运行,相关的事件处理程序的逻辑简单概括就是如下两点:

  • 计算当前点击位置的相对Content节点的位置坐标:先计算Content DOM节点的偏移位置数据,相对Content的位置坐标 = 当前点击位置的屏幕坐标 - Content偏移位置
  • 根据位置坐标然后根据相关逻辑得到当前对应的Shape对象
  • 触发Shape的fireAndBubble实例方法,该方法内部会实现事件冒泡,即查找当前Shape是否存在父节点,只要存在父节点就会一直调用fire实例方法触发事件事件

通过上面的机制从而实现事件冒泡,并且Konva还提供listening属性来实现对应图形对象是否触发事件。

图形命中策略

在上小结事件响应中根据位置坐标找到对应的Shape对象这个逻辑并没有细说,实际上这部分逻辑非常重要,这里细细说明。实际上这部分的逻辑是在getIntersection实例方法中,该实例方法的核心逻辑如下图所示:
Konva事件机制_第2张图片
当根据位置选中图形对象就会遍历所有的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的图形绘制都会使用唯一色值填充,并将这个颜色值保存到集合中,这种方案可以很准确的处理复杂图形的选中,但是也存在相应的问题:

  1. 额外增加HitCanvas绘制图形,增大内存使用,并且每次图形更新都要额外处理HitCanvas
  2. 颜色值作为唯一键值,其大小是有限的,即255 * 255 * 255,当然这个数量级页面本身也会存在性能问题了

实际上目前对于Canvas图形拾取策略除了Konva这种色值法方案,还有几何计算法,具体的优缺点比较可以查看这篇文章
Canvas 的拾取方案选择。

你可能感兴趣的:(图形渲染相关,konva,事件机制,源码分析)