• 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    上面案例实现对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,
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    每个继承自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']
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    click事件会触发_pointerdown、_pointerup,主要的处理逻辑总结如下:
    click处理逻辑
    从Stage Content DOM节点接收事件触发相关事件处理程序运行,相关的事件处理程序的逻辑简单概括就是如下两点:

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

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

    图形命中策略

    在上小结事件响应中根据位置坐标找到对应的Shape对象这个逻辑并没有细说,实际上这部分逻辑非常重要,这里细细说明。实际上这部分的逻辑是在getIntersection实例方法中,该实例方法的核心逻辑如下图所示:
    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,
                        };
                    }
                }
                ...
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从上面逻辑可以看出两点核心处理:

    • 会调用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;
            }
            ...
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    所有继承自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 的拾取方案选择

  • 相关阅读:
    【1460. 通过翻转子数组使两个数组相等】
    一文2000字手把手教你自动化测试平台建设分享
    [Ansible专栏]Ansible常用模块介绍和使用
    kuiper安装
    01 # 手写 new 的原理
    Spring Boot从入门到精通【一】
    Solidity 小白教程:18. Import
    Java版企业电子招标采购系统源码—企业战略布局下的采购寻源
    MySQL中的分组函数和分组查询
    npm install 时候 停留在某个git 资源依赖上
  • 原文地址:https://blog.csdn.net/s1879046/article/details/133350538