前端推荐!10分钟带你了解Konva运行原理

腾小云 2021-10-13 17:06:27
前端 推荐 分钟 带你 了解


导语 | Konva是一个很优秀的Canvas框架,API封装简洁易懂,基于TypeScript实现,有React和Vue版本。本文总结梳理了Konva的架构设计、原理及其缺点,希望可以为大家了解KonvaJS的相关问题提供一些参考。

一、前言

用过Canvas的都知道它的API比较多,使用起来也很麻烦,比如我想绘制一个圆形就要调一堆API,对开发算不上友好。

const canvas = document.querySelector('canvas');const context = canvas.getContext('2d');// 设置字体样式context.font = '24px SimSun, Songti SC';context.fillText('24px的宋体呈现', 20, 50);// 绘制完整圆context.fillStyle = 'RGB(255, 0, 0)';context.beginPath();context.arc(150, 75, 50, 0, Math.PI * 2);context.stroke();

为了解决这个痛点,诞生了例如PIXI、ZRender、Fabric等Canvas库。今天要讲的Konva也是一个很优秀的Canvas框架,API封装简洁易懂,基于TypeScript实现,有React和Vue版本。

 const stage = new Konva.Stage({ container: 'root', width: 1000, height: 1000, }); const layer = new Konva.Layer(); const group = new Konva.Group(); const text = new Konva.Text({ text: 'Hello, this is some good text', fontSize: 30, });
const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 70, fill: 'red', stroke: 'black', strokeWidth: 4 }); group.add(text); group.add(circle); layer.add(group); stage.add(layer);

二、架构设计

(一)Konva Tree

从前言里面给的那段代码可以看出来,Konva有一定的嵌套结构,有些类似DOM结构。通过add和remove就能实现子节点的添加和删除。

Konva Tree主要包括这么四部分:

  1. Stage根节点:这是应用的根节点,会创建一个div节点,作为事件的接收层,根据事件触发时的坐标来分发出去。一个Stage节点可以包含多个Layer图层。
  2. Layer图层:Layer里面会创建一个Canvas节点,主要作用就是绘制Canvas里面的元素。一个Layer可以包含多个Group和Shape。
  3. Group组:Group包含多个Shape,如果对其进行变换和滤镜,里面所有的Shape都会生效。
  4. Shape:指Text、Rect、Circle等图形,这些是Konva封装好的类。

(二)build dom

Stage创建的时候会去创建两个Canvas节点以及content容器节点,这两个Canvas节点是用于perfectDrawEnabled的,后面会讲到。

这里需要注意的就是这个content节点,作为整个Konva画布的容器,之后的Layer都会被append进去。

 _buildDOM() { this.bufferCanvas = new SceneCanvas({ width: this.width(), height: this.height(), }); this.bufferHitCanvas = new HitCanvas({ pixelRatio: 1, width: this.width(), height: this.height(), });
if (!Konva.isBrowser) { return; } var container = this.container(); if (!container) { throw 'Stage has no container. A container is required.'; } // clear content inside container container.innerHTML = '';
// content this.content = document.createElement('div'); this.content.style.position = 'relative'; this.content.style.userSelect = 'none'; this.content.className = 'konvajs-content';
this.content.setAttribute('role', 'presentation');
container.appendChild(this.content);
this._resizeDOM(); }

在调用Stage.add的时候,不仅会调用Layer的绘制方法,还会把Layer的Canvas节点append进去。

 add(layer: Layer, ...rest) { if (arguments.length > 1) { for (var i = 0; i < arguments.length; i++) { this.add(arguments[i]); } return this; } super.add(layer);
var length = this.children.length; if (length > MAX_LAYERS_NUMBER) { Util.warn( 'The stage has ' + length + ' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.' ); } layer.setSize({ width: this.width(), height: this.height() });
// draw layer and append canvas to container layer.draw();
if (Konva.isBrowser) { this.content.appendChild(layer.canvas._canvas); }
// chainable return this; }

三、渲染

(一)批量渲染

从前面的代码中可以看到,没有手动调用绘制方法,但依然会进行绘制,说明会在一定的时机进行渲染。这个时机就在add方法里面,不管Group、Layer、Stage哪个先add,最终都会触发渲染。

他们三个都继承了Container类,在Container类里面有一个add方法,我们来一探究竟。

 add(...children: ChildType[]) { if (arguments.length > 1) { for (var i = 0; i < arguments.length; i++) { this.add(arguments[i]); } return this; } var child = children[0]; // 如果要添加的子节点已经有个父节点,那就先将其从父节点移除,再插入到当前节点里面 if (child.getParent()) { child.moveTo(this); return this; } this._validateAdd(child); // 设置子节点的 index 和 parent child.index = this.getChildren().length; child.parent = this; child._clearCaches(); this.getChildren().push(child); this._fire('add', { child: child, }); // 请求绘制 this._requestDraw(); return this; }

除了一些常规的处理之外,渲染的关键就在_requestDraw方法里面。这里调用了Layer上面的batchDraw进行批量重绘。

 _requestDraw() { if (Konva.autoDrawEnabled) { const drawNode = this.getLayer() || this.getStage(); drawNode?.batchDraw(); } }

这个批量重绘的原理是利用requestAnimationFrame方法将要绘制的内容放到下一帧来绘制。这样同时修改多个图形多个属性就不需要反复绘制了。

 batchDraw() { // _waitingForDraw 保证只会执行一次 requestAnimFrame if (!this._waitingForDraw) { this._waitingForDraw = true; // 如果调用多次方法修改 Shape 属性,这里就会批量绘制 // 避免了多次绘制带来的开销 Util.requestAnimFrame(() => { this.draw(); this._waitingForDraw = false; }); } return this; }

(二)Shape绘制

所有涉及到图形绘制的地方都是调用Shape实现类上的_sceneFunc方法,以Circle为例:

 _sceneFunc(context) { context.beginPath(); context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false); context.closePath(); context.fillStrokeShape(this); }

在Shape和Node两个基类上面只负责调用,具体的实现放到具体的Shape实现上面。这样带来两个好处,一个是可以实现自定义图形,另一个是以后要是支持SVG、WebGL会很方便。

(三)离屏渲染

什么是离屏渲染?就是在屏幕之外预渲染一个Canvas,之后通过drawImage的形式将其绘制到屏幕要显示的Canvas上面,对形状相似或者重复的对象绘制性能提升非常高

假设我们有个列表页,每次滚动的时候全部重新绘制开销会比较大。但如果我们实现一个Canvas池,把已经绘制过的列表项存起来。下次滚动到这里的时候,就可以直接从Canvas池里面取出来drawImage到页面上了。

在Node类上面有个cache方法,这个方法可以实现细粒度的离屏渲染。cache方法内部会创建三个Canvas,分别是:

  • cachedSceneCanvas:用于绘制图形的Canvas的离屏渲染。
  • cachedFilterCanvas:用于处理滤镜效果。
  • cachedHitCanvas:用于处理hitCanvas的离屏渲染。
 _drawCachedSceneCanvas(context: Context) { context.save(); context._applyOpacity(this); context._applyGlobalCompositeOperation(this); // 获取离屏的 Canvas const canvasCache = this._getCanvasCache(); context.translate(canvasCache.x, canvasCache.y);
var cacheCanvas = this._getCachedSceneCanvas(); var ratio = cacheCanvas.pixelRatio; // 将离屏 Canvas 绘制到要展示的 Canvas 上面 context.drawImage( cacheCanvas._canvas, 0, 0, cacheCanvas.width / ratio, cacheCanvas.height / ratio ); context.restore(); }

(四)perfectDrawEnabled

Canvas在绘制stroke和fill的时候,如果遇到透明度的时候,stroke会和fill的一部分重合到一起,就不符合我们的预期了。

比如下面这段代码:

 const canvas = document.getElementById("canvas"); const bufferCanvas = document.createElement("canvas"); const bufferCtx = bufferCanvas.getContext("2d"); const ctx = canvas.getContext("2d");
ctx.strokeStyle="green"; ctx.lineWidth=10; ctx.strokeRect(30,30,50,50); ctx.globalAlpha = 0.5; ctx.fillStyle="RGB(255, 0, 0)"; ctx.fillRect(30,30,50,50);

它的实际展示效果是这样的,中间的stroke和fill有一部分重叠。

在这种情况下,KonvaJS实现了一个perfectDrawEnabled功能,它会这样做:

  • 在 bufferCanvas上绘制Shape
  • 绘制fill和stroke
  • 在layer上应用透明度
  • 将bufferCanvas绘制到sceneCanvas上面

可以看到开启perfectDrawEnabled和关闭perfectDrawEnabled的区别很明显:

它会在Stage里面创建一个bufferCanvas和bufferHitCanvas,前者就是针对sceneCanvas的,后者是针对hitCanvas的。

在Shape的drawScene方法里面,会判断是否使用bufferCanvas:

 // if buffer canvas is needed if (this._useBufferCanvas() && !skipBuffer) { stage = this.getStage(); bufferCanvas = stage.bufferCanvas; bufferContext = bufferCanvas.getContext(); bufferContext.clear(); bufferContext.save(); bufferContext._applyLineJoin(this); // layer might be undefined if we are using cache before adding to layer var o = this.getAbsoluteTransform(top).getMatrix(); bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]); // 在 bufferCanvas 绘制 fill 和 stroke drawFunc.call(this, bufferContext, this); bufferContext.restore();
var ratio = bufferCanvas.pixelRatio;
if (hasShadow) { context._applyShadow(this); } // 在 sceneCanvas 应用透明度 context._applyOpacity(this); context._applyGlobalCompositeOperation(this); // 将 bufferCanvas 绘制到 sceneCanvas context.drawImage( bufferCanvas._canvas, 0, 0, bufferCanvas.width / ratio, bufferCanvas.height / ratio ); }

四、事件

Konva里面的事件是在Canvas外层创建了一个div节点,在这个节点上面接收了DOM事件,再根据坐标点来判断当前点击的是哪个Shape,然后进行事件分发。

所以关键就在如何判断当前点击的Shape是哪个?相比ZRender里面比较复杂的计算,Konva使用了一个相当巧妙的方式。

(一)事件分发

Konva目前支持下面这么多事件,EVENTS是事件名-事件处理方法的映射。

EVENTS = [ [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'], ]; // 绑定事件 _bindContentEvents() { if (!Konva.isBrowser) { return; } EVENTS.forEach(([event, methodName]) => { // 事件绑定在 content 这个 dom 节点上面 this.content.addEventListener(event, (evt) => { this[methodName](evt); }); }); }

我们以mousedown这个具体的事件作为例子来分析,它的处理方法在_pointerdown里面。_pointerdown先执行setPointersPositions,计算当前鼠标点击的坐标,减去content相对页面的坐标,得到了当前点击相对于content的坐标。同时将其存入了_changedPointerPositions 里面。

然后遍历_changedPointerPositions,通过getIntersection获取到了点击的Shape图形。这个getIntersection遍历调用了每个Layer的getIntersection方法,通过Layer获取到了对应的Shape。

因为可以存在多个Layer,每个Layer也可以在同一个位置绘制多个Shape,所以理论上可以获取到多个Shape,Konva这里只取了第一个Shape,按照Layer->Shape的顺序来的。

然后Stage会调用Shape上面的_fireAndBubble方法,这个方法调用_fire发送Konva自己的事件,此时通过on绑定的事件回调就会触发,有点儿像jQuery那样。

然后Konva会继续往上找到父节点,继续调用父节点的_fireAndBubble方法,直到再也找不到父节点为止,这样就实现了事件冒泡。

对于不想被点击到的Shape来说,可以设置isListening属性为false,这样事件就不会触发了。

(二)匹配Shape

那么Layer是怎么根据点击坐标获取到对应的Shape呢?如果是规则的图形(矩形、圆形)还比较容易计算,要是下面这种不规则图形呢?

众所周知,在Canvas里面有个getImageData方法,它会根据传入的坐标来返回一个ImageData信息,里面有当前坐标对应的色值。那么我们能不能根据这个色值来获取到对应的Shape呢?

canvas = new SceneCanvas();hitCanvas = new HitCanvas({ pixelRatio: 1,});
 constructor(config?: Config) { super(config); // set colorKey let key: string;
while (true) { // 生成随机色值 key = Util.getRandomColor(); if (key && !(key in shapes)) { break; } } this.colorKey = key; // 存入 shapes 数组 shapes[key] = this; }

每次在sceneCanvas上面绘制的时候,同样会在内存中的hitCanvas里面绘制一遍,并且将上面随机生成的色值作为fill和stroke的颜色填充。

当点击sceneCanvas的时候,获取到点击的坐标点,通过调用hitCanvas 的getImageData就可以获取到colorKey,然后再通过colorKey就能找到对应的Shape了,真是相当巧妙的实现。

 drawHit(can?: HitCanvas, top?: Node, skipDragCheck = false) { if (!this.shouldDrawHit(top, skipDragCheck)) { return this; }
var layer = this.getLayer(), canvas = can || layer.hitCanvas, context = canvas && canvas.getContext(), // 如果有 hitFunc,就不使用 sceneFunc drawFunc = this.hitFunc() || this.sceneFunc(), cachedCanvas = this._getCanvasCache(), cachedHitCanvas = cachedCanvas && cachedCanvas.hit;
if (!this.colorKey) { Util.warn( 'Looks like your canvas has a destroyed shape in it. Do not reuse shape after you destroyed it. If you want to reuse shape you should call remove() instead of destroy()' ); } // ... drawFunc.call(this, context, this); // ...}

(三)拖拽事件

Konva的拖拽事件没有使用原生的方法,而是基于mousemove和touchmove来计算移动的距离,进而手动设置Shape的位置,实现逻辑比较简单,这里不细说。

五、滤镜

Konva支持多种滤镜,在使用滤镜之前需要先将Shape cache起来,然后使用filter() 方法添加滤镜。在cache里面除了创建用于离屏渲染的Canvas,还会创建滤镜Canvas。滤镜处理在_getCachedSceneCanvas里面。

首先将sceneCanvas通过drawImage绘制到filterCanvas上面,接着filterCanvas获取所有的ImageData,遍历所有设置的滤镜方法,将ImageData传给滤镜方法来处理。

处理完ImageData之后,再将其通过putImageData绘制到filterCanvas上面。

 if (filters) { if (!this._filterUpToDate) { var ratio = sceneCanvas.pixelRatio; filterCanvas.setSize( sceneCanvas.width / sceneCanvas.pixelRatio, sceneCanvas.height / sceneCanvas.pixelRatio ); try { len = filters.length; filterContext.clear();
// copy cached canvas onto filter context filterContext.drawImage( sceneCanvas._canvas, 0, 0, sceneCanvas.getWidth() / ratio, sceneCanvas.getHeight() / ratio ); imageData = filterContext.getImageData( 0, 0, filterCanvas.getWidth(), filterCanvas.getHeight() );
// apply filters to filter context for (n = 0; n < len; n++) { filter = filters[n]; if (typeof filter !== 'function') { Util.error( 'Filter should be type of function, but got ' + typeof filter + ' instead. Please check correct filters' ); continue; } filter.call(this, imageData); filterContext.putImageData(imageData, 0, 0); } } catch (e) { Util.error( 'Unable to apply filter. ' + e.message + ' This post my help you https://konvajs.org/docs/posts/Tainted_Canvas.html.' ); }
this._filterUpToDate = true; }
return filterCanvas; }

那滤镜效果怎么画上去的呢?在konva里面进行了特殊处理,如果存在filterCanvas,那就不会使用cacheCanvas了,也就是我们原本用于缓存的离屏Canvas会被filterCanvas进行替代。

最终filterCanvas会通过drawImage的方式绘制到sceneCanvas上面。

六、选择器

Konva实现了选择器,方便我们快速查找到某个Shape。目前主要有三种选择器,分别是id选择器、name选择器、type选择器。

前两者需要在实例化的时候传入一个id或者name属性,后者则是根据类名(Rect、Line等)来查找的。

选择器查找的时候需要调用find方法,这个find方法挂载在Container 类上面。它调用了_descendants进行子节点的遍历,将遍历的node节点调用isMatch方法来判断是否匹配上。

 _generalFind( selector: string | Function, findOne: boolean ) { var retArr: Array = []; // 调用 _descendants 获取所有的子节点 this._descendants((node: ChildNode) => { const valid = node._isMatch(selector); if (valid) { retArr.push(node); } // 如果是 findOne,后面的就不继续执行了 if (valid && findOne) { return true; } return false; });
return retArr; } private _descendants(fn: (n: Node) => boolean) { let shouldStop = false; const children = this.getChildren(); for (const child of children) { shouldStop = fn(child); if (shouldStop) { return true; } if (!child.hasChildren()) { continue; } // 如果子节点也有子节点,那就递归遍历 shouldStop = (child as any)._descendants(fn); // 如果应该停止查找(一般是 findOne 的时候就不需要查找后面的了) if (shouldStop) { return true; } } return false; }</childnode extends node = node>

在isMatch里面可以看到后根据是什么类型的选择器来分别进行匹配。

 // id selector if (sel.charAt(0) === '#') { if (this.id() === sel.slice(1)) { return true; } } else if (sel.charAt(0) === '.') { // name selector if (this.hasName(sel.slice(1))) { return true; } } else if (this.className === sel || this.nodeType === sel) { return true; }

七、序列化

Konva还支持对Stage的序列化和反序列化,简单来说就是把Stage的数据导出成一份JSON数据以及把JSON数据导入,方便我们在NodeJS端进行服务端渲染。

序列化主要在toObject方法里面,它会对函数和DOM节点进行过滤,只保留一份描述信息,比如Layer的信息、Shape的信息等等,有点儿类似 React里面的Virtual DOM。

 toObject() { var obj = {} as any, attrs = this.getAttrs(), key, val, getter, defaultValue, nonPlainObject;
obj.attrs = {};
for (key in attrs) { val = attrs[key]; nonPlainObject = Util.isObject(val) && !Util._isPlainObject(val) && !Util._isArray(val); if (nonPlainObject) { continue; } getter = typeof this[key] === 'function' && this[key]; delete attrs[key]; // 特殊处理函数,将其执行后把结果挂载到当前key上面 defaultValue = getter ? getter.call(this) : null; // restore attr value attrs[key] = val; if (defaultValue !== val) { obj.attrs[key] = val; } }
obj.className = this.getClassName(); return Util._prepareToStringify(obj); }

反序列化则是对传入的JSON信息进行解析,根据className来创建不同的对象,对深层结构进行递归,然后add到父节点里面

 static _createNode(obj, container?) { var className = Node.prototype.getClassName.call(obj), children = obj.children, no, len, n;
// if container was passed in, add it to attrs if (container) { obj.attrs.container = container; }
if (!Konva[className]) { Util.warn( 'Can not find a node with class name "' + className + '". Fallback to "Shape".' ); className = 'Shape'; } // 根据传入的 className 来实例化 const Class = Konva[className];
no = new Class(obj.attrs); if (children) { len = children.length; for (n = 0; n < len; n++) { // 如果还有子节点,那就递归创建 no.add(Node._createNode(children[n])); } }
return no; }

八、React

Konva和React绑定没有使用重新封装一遍组件的方式,而是采用了和react-dom、react-native一样的形式,基于react-reconciler来实现一套hostConfig,从而定制自己的Host Component(宿主组件)。

(一)react-reconciler

React Fiber架构诞生之后,他们就将原来的React核心代码做了抽离。主要包括react、react-reconciler和platform实现(react-dom、react-native等)三部分。

在react-reconciler里面实现了大名鼎鼎的Diff算法、时间切片、调度等等,它还暴露给了我们一个hostConfig文件,允许我们在各种钩子函数中实现自己的渲染。

在React里面,有两种组件类型,一种是Host Component(宿主组件),另一种是Composition Component(复合组件)。

在DOM里面,前者就是h1、div、span等元素,在react-native里面,前者就是View、Text、ScrollView等元素。后者则是我们基于Host Component自定义的组件,比如App、Header等等。

在react-reconciler里面,它允许我们去自定义Host Component的渲染(增删查改),这也意味着跨平台的能力。我们只需要编写一份hostConfig文件,就能够实现自己的渲染。

参考上面的架构图,会发现不管是渲染到native、Canvas,甚至是小程序都可以。业界已经有方案是基于这个来实现了。

(二)react-konva

react-konva的主要实现就在ReactKonvaHostConfig.js里面,它利用Konva原本的API实现了对Virtual DOM的映射,响应了Virtual DOM的增删查改。

这里从中抽取了部分源码:

// 创建一个实例export function createInstance(type, props, internalInstanceHandle) { let NodeClass = Konva[type];
const propsWithoutEvents = {}; const propsWithOnlyEvents = {};
for (var key in props) { var isEvent = key.slice(0, 2) === 'on'; if (isEvent) { propsWithOnlyEvents[key] = props[key]; } else { propsWithoutEvents[key] = props[key]; } } // 根据传入的 type 来创建一个实例,相当于 new Layer、new Rect 等 const instance = new NodeClass(propsWithoutEvents); // 将传入的 props 设置到实例上面 // 如果是普通的 prop,就直接通过 instance.setAttr 更新 // 如果是 onClick 之类的事件,就通过 instance.on 来绑定 applyNodeProps(instance, propsWithOnlyEvents);
return instance;}// 插入子节点,直接调用 konva 的 add 方法export function appendChild(parentInstance, child) { if (child.parent === parentInstance) { child.moveToTop(); } else { parentInstance.add(child); }
updatePicture(parentInstance);}
// 移除子节点,直接调用 destroy 方法export function removeChild(parentInstance, child) { child.destroy(); child.off(EVENTS_NAMESPACE); updatePicture(parentInstance);}
// 通过设置 zIndex 实现 insertBeforeexport function insertBefore(parentInstance, child, beforeChild) { // child._remove() will not stop dragging // but child.remove() will stop it, but we don't need it // removing will reset zIndexes child._remove(); parentInstance.add(child); child.setZIndex(beforeChild.getZIndex()); updatePicture(parentInstance);}

九、Vue-Konva

在Vue上面,Konva通过Vue.use注册了一个插件,这个插件里面分别注册了每个组件。

const components = [ { name: 'Stage', component: Stage }, ...KONVA_NODES.map(name => ({ name, component: KonvaNode(name) }))];const VueKonva = { install: (Vue, options) => { let prefixToUse = componentPrefix; if(options && options.prefix){ prefixToUse = options.prefix; } components.forEach(k => { Vue.component(`${prefixToUse}${k.name}`, k.component); }) }};
export default VueKonva;
if (typeof window !== 'undefined' && window.Vue) { window.Vue.use(VueKonva);}

再来看看KonvaNode的实现,在KonvaNode里面,对于节点的增删查改都在Vue的生命周期里面实现的。在Vue的created生命周期里面调用initKonva去new一个NodeClass,和上面React的方式几乎一样。

 initKonva() { const NodeClass = window.Konva[nameNode];
if (!NodeClass) { console.error('vue-konva error: Can not find node ' + nameNode); return; }
this._konvaNode = new NodeClass(); this._konvaNode.VueComponent = this;
this.uploadKonva(); },

而在Updated的时候去进行Props的更新,在destroyed里面对节点进行destroy,实现上更加简洁一些。

 updated() { this.uploadKonva(); checkOrder(this.$vnode, this._konvaNode); }, destroyed() { updatePicture(this._konvaNode); this._konvaNode.destroy(); this._konvaNode.off(EVENTS_NAMESPACE); },

十、缺陷

脏矩形

在性能方面,Konva对比PIXI、ZRender这些库还是不太够看。如果我们Layer上有非常多的Shape,如果想更新某个Shape,按照Konva的实现方式依然会全量绘制。

虽然Konva支持单个Shape重绘,但实现上是无脑覆盖原来的位置,这也意味着如果你的图形在其他节点图形下面,就会出现问题。

所以这里缺少非常重要的局部更新能力,也就是我们常说的脏矩形。

脏矩形就是指当我们更新一个Shape的时候,利用碰撞检测计算出和他相交的所有Shape,将其进行合并,计算出一块儿脏区域。然后我们通过clip限制Canvas只在这块儿脏区进行绘制,这样就实现了局部更新。

可惜Konva的包围盒实现的非常简单,不适合做碰撞检测,它也没有提供脏矩形的能力。

 作者简介

尹光耀

腾讯文档前端工程师

腾讯文档前端工程师,腾讯校企合作讲师,毕业于武汉大学,个人公众号—前端小馆,慕课网《Web 前端开发修炼指南》作者。目前负责腾讯文档渲染层开发工作,有丰富的移动Web开发经验,深入React全家桶原理。

 推荐阅读

Golang原生json可以一库走天下吗?

这次全了,8种超详细Web跨域解决方案!

10分钟带你玩转Kafka基于Controller的领导选举!

LLVM极简教程:9个步骤!实现一个简单编译器


本文分享自微信公众号 - 云加社区(QcloudCommunity)

原文出处及转载信息见文内详细说明,如有侵权,请联系 [email protected] 删除。

原始发表时间: 2021-10-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

版权声明
本文为[腾小云]所创,转载请带上原文链接,感谢
https://cloud.tencent.com/developer/article/1888589

  1. Projet Java: système de gestion du rendement des employés (Java + SSM + MySQL + Maven + HTML)
  2. CSS tips | one line of code to realize the integration of avatar and national flag
  3. Maotai and Paris Fashion Week joined hands to make Chinese elements appear on the show
  4. Wang Xiaoya showed up in a sleeveless skirt and reappeared her intellectual elegance. She was still full of temperament after leaving the nest CCTV
  5. Comment écrire un document de conception frontale
  6. Créer une api javascript haute performance avec Rust et l'exécuter dans webassembly
  7. Analyse de certains principes techniques clés du SDK de surveillance frontale
  8. Point de vue: la NFT de type portrait a formé un modèle d'entreprise. Quelles sont ses perspectives d'avenir et ses difficultés?
  9. Stars celebrate the motherland's birthday in patterns: Tang Yan Bixin, Liu Xiaoqing in military uniform, Zhao Liying and he Jiong send blessings
  10. L'amour entre Wing Mei et Luan Tree: de l'amour à première vue à l'amour éternel
  11. Disappeared car companies: tape measure is useless? Zhongtai is on the verge of death and has no way to return to heaven
  12. BUUCTF [极客大挑战 2019]Http
  13. element缓存到本地使用
  14. How can the volunteer army with less steel and more gas beat the American army with more steel and less gas? Changjin Lake gives you the answer
  15. CentOS installation source package nginx error
  16. Mise en cache des éléments pour utilisation locale
  17. Disappeared car companies: tape measure is useless? Zhongtai is on the verge of death and has no way to return to heaven
  18. He saifei est si naturel!58 ans est si beau, porter des vêtements de vieillesse pour accepter de vieillir!
  19. Finally pregnant! Seven years of pregnancy, collective blessing of the entertainment industry
  20. Wu Qili sent blessings on the national day. She looked haggard and thin on her own. It was worrying
  21. Error in spring source code compilation: the monoprocessor in reactor.core.publisher is outdated
  22. Buctf [geek Challenge 2019] http
  23. 20 个值得学习的 Vue 开源项目
  24. 20 projets open source à apprendre
  25. Scène et application de la manette des gaz anti - bavardage
  26. Qu'est - ce que j'ai gagné en abandonnant vue pour les six mois de React?
  27. À partir de [Bytecode cache] et de [http cache], intervieweur: « est - ce si mince? »
  28. [niveau intermédiaire et avancé] obligatoire, 30 + questions manuscrites à haute fréquence et réponses détaillées (dix mille caractères longs), voyez comment "vous" ne pouvez pas m'abattre
  29. Mise en œuvre d'un outil d'échafaudage universel pour l'ingénierie Web de 0 à 1
  30. 【中高级前端】必备,30+高频手写题及详细答案(万字长文),看“你”怎么难倒我
  31. In less than two days, the box office exceeded 400 million, and Changjin Lake broke out, breaking seven records in Chinese film history
  32. Tong Liya Jin Chen bumps her hair, Xie Na Zhao Liying bumps her shirt, and she sees EQ from the reaction
  33. react之组件生命周期
  34. L'équipe de vue dévoile un nouvel outil d'échafaudage rapide comme la foudre Create View, qui remplacera la vue CLI à l'avenir, avec seulement 300 lignes de code, apprenez - le!
  35. 20 dessins illustrant le fonctionnement du moteur de rendu du Navigateur
  36. In less than two days, the box office exceeded 400 million, and Changjin Lake broke out, breaking seven records in Chinese film history
  37. 千锋重庆web前端学习之理解CSS位置属性
  38. 什么是语义HTML标记以及如何使用它们?
  39. Si vous vous représentez avec un tableau, c'est le champ de Van Gogh.
  40. 前端面试手写代码
  41. 前端开发框架Vue中Vuex的使用原理分享
  42. vue-echarts初次体验
  43. 分享一些web前端开发好用的网站
  44. 每天读一点webpack-003
  45. react之组件生命周期
  46. Alibaba collection version of mybatis handwritten documents, Java front-end interview questions
  47. SpringBoot Java后端实现okhttp3超时设置
  48. react之組件生命周期
  49. Cycle de vie des composants de React
  50. 使用Reactor将阻塞调用变为异步非阻塞
  51. Baked cake wife sun photos, plain face on camera, beautiful appearance is still a beauty, watching children during the festival is a little helpless
  52. 亚洲知名插画师荒川(arakawa) 仅8件独版NFT作品系列《Can't Out》正式上架Element综合市场
  53. Taiyuan: singing, welcoming the national day, gathering to praise blessings
  54. Arakawa, un illustrateur Asiatique bien connu, n'a mis sur le marché que huit pièces de la collection NFT "can't out" en une seule édition.
  55. Résumé des questions d'entrevue Hadoop (II) - - hdfs
  56. 如何解决“Serverless”系统的冷启动问题
  57. BootstrapBlazor 模板安装
  58. BootstrapBlazor 模板安装
  59. Tong Liya Jin Chen bumps her hair, Xie Na Zhao Liying bumps her shirt, and she sees EQ from the reaction
  60. 使用ESLint+Prettier来统一前端代码风格