Preface
Anyone who has used Canvas knows that it has a lot of APIs, and it is very troublesome to use. For example, if I want to draw a circle, I need to adjust a lot of APIs, which is not friendly to development.
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();
In order to solve this pain point, Canvas libraries such as PIXI, ZRender, and Fabric were born. Konva I'm going to talk about today is also an excellent Canvas framework. The API package is simple and easy to understand. It is implemented based on TypeScript and has React and Vue versions.
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);
Architecture design
Konva Tree
From the code given in the preface, it can be seen that Konva has a certain nested structure, which is somewhat similar to the DOM structure. Add and delete child nodes can be realized through add and remove.
Konva Tree mainly includes several parts:
- Stage root node: This is the root node of the application. A div node will be created as the receiving layer of the event and distributed according to the coordinates when the event is triggered. A Stage node can contain multiple Layer layers.
- Layer: A Canvas node will be created in Layer, whose main function is to draw elements in Canvas. A layer can contain multiple groups and shapes.
- Group group: Group contains multiple shapes, if you transform and filter them, all the shapes in it will take effect.
- Shape: Refers to shapes such as Text, Rect, Circle, etc. These are classes encapsulated by Konva.
build dom
When the stage is created, two Canvas nodes and content container nodes are created. These two Canvas nodes are used for perfectDrawEnabled, which will be discussed later.
What needs to be noted here is that this content node serves as a container for the entire Konva canvas, and subsequent Layers will be appended.
_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();
}
When Stage.add is called, not only will the drawing method of the layer be called, but also the Canvas node of the layer will be appended.
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;
}
Rendering
Batch rendering
As you can see from the previous code, the drawing method is not manually called, but drawing will still be performed, indicating that it will be rendered at a certain timing.
This timing is in the add method. No matter which of Group, Layer, or Stage is added first, rendering will eventually be triggered.
All three of them inherit the Container class. There is an add method in the Container class. Let's find out.
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;
}
In addition to some conventional processing, the key to rendering lies in the _requestDraw
method. Here, batchDraw
on Layer is called for batch redrawing.
_requestDraw() {
if (Konva.autoDrawEnabled) {
const drawNode = this.getLayer() || this.getStage();
drawNode?.batchDraw();
}
}
The principle of this batch redraw is to use the requestAnimationFrame method to put the content to be drawn on the next frame for drawing. In this way, you don't need to draw repeatedly to modify multiple attributes of multiple graphics at the same time.
batchDraw() {
// _waitingForDraw 保证只会执行一次 requestAnimFrame
if (!this._waitingForDraw) {
this._waitingForDraw = true;
// 如果调用多次方法修改 Shape 属性,这里就会批量绘制
// 避免了多次绘制带来的开销
Util.requestAnimFrame(() => {
this.draw();
this._waitingForDraw = false;
});
}
return this;
}
Shape drawing
All the places involved in drawing graphics are calling the _sceneFunc
method on the Shape implementation class. Take Circle as an example:
_sceneFunc(context) {
context.beginPath();
context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false);
context.closePath();
context.fillStrokeShape(this);
}
The two base classes of Shape and Node are only responsible for calling, and the specific implementation is placed on the specific Shape implementation. This brings two benefits, one is that custom graphics can be implemented, and the other is that it will be very convenient to support SVG and WebGL in the future.
Off-screen rendering
What is off-screen rendering? It is to pre-render a Canvas outside the screen, and then draw it on the Canvas to be displayed on the screen in the form of drawImage, which improves the drawing performance of objects with similar shapes or repetitions very high.
Assuming we have a list page, it will be expensive to redraw it every time it is scrolled. But if we implement a Canvas pool, store the list items that have been drawn. Next time you scroll here, you can directly take out drawImage from the Canvas pool and put it on the page.
There is a cache method on the Node class, which can achieve fine-grained off-screen rendering.
Three canvases are created inside the cache method, namely:
- cachedSceneCanvas: Off-screen rendering of canvas used to draw graphics.
- cachedFilterCanvas: Used to process filter effects.
- cachedHitCanvas: Used to process the off-screen rendering of 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
When drawing stroke and fill in Canvas, if it encounters transparency, part of the stroke and fill will overlap, which does not meet our expectations.
For example, the following code:
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);
Its actual display effect is like this, the stroke and fill in the middle partly overlap.
In this case, KonvaJS implements a perfectDrawEnabled function, which will do this:
- Draw Shape on bufferCanvas
- Draw fill and stroke
- Apply transparency on the layer
- Draw the bufferCanvas onto the sceneCanvas
You can see that the difference between turning on perfectDrawEnabled and turning off perfectDrawEnabled is obvious:
It will create a bufferCanvas and bufferHitCanvas in the Stage. The former is for sceneCanvas and the latter is for hitCanvas.
In the drawScene method of Shape, it will determine whether to use 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
);
}
event
The event in Konva creates a div node on the outer layer of Canvas, receives DOM events on this node, and then determines which Shape is currently clicked according to the coordinate points, and then distributes the event.
So the key is how to judge which Shape is currently clicked? Compared to the more complex calculations in ZRender, Konva uses a rather clever way.
Event distribution
Konva currently supports the following events. EVENTS is a mapping event name-event processing method.
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);
});
});
}
We take the specific event of mousedown as an example to analyze, and its processing method is in _pointerdown
._pointerdown
first executes setPointersPositions
, calculates the coordinates of the current mouse click, subtracts the coordinates of the content relative to the page, and obtains the coordinates of the current click relative to the content. At the same time, it was stored in _changedPointerPositions
.
Then traverse _changedPointerPositions
and get the clicked Shape graphic getIntersection
This getIntersection
traversal calls the getIntersection
method of each Layer, and the corresponding Shape is obtained through the Layer.
Because there can be multiple layers, each layer can also draw multiple shapes at the same position, so in theory, you can get multiple shapes, Konva only takes the first shape here, in the order of Layer -> Shape .
Then the Stage will call the _fireAndBubble
method on the Shape. This method calls _fire
send Konva's own events. At this time, the event callbacks bound by on will be triggered, a bit like jQuery.
Then Konva will continue to find the parent node and continue to call the _fireAndBubble
method of the parent node until the parent node is no longer found, thus achieving event bubbling.
isListening
that do not want to be clicked, you can set the 061633137610bf attribute to false, so that the event will not be triggered.
Match Shape
So how does Layer get the corresponding Shape according to the click coordinates? If it is a regular figure (rectangle, circle), it is easier to calculate. What if the following irregular figure?
As we all know, there is a getImageData
method in Canvas, which will return an ImageData information according to the incoming coordinates, which contains the color value corresponding to the current coordinates. So can we get the corresponding Shape based on this color value?
Therefore, when Konva creates the Layer, it creates two Canvas, one for the sceneCanvas to draw the Shape, and the other for the hitCanvas in the memory to determine whether it has been hit.
canvas = new SceneCanvas();
hitCanvas = new HitCanvas({
pixelRatio: 1,
});
When Shape is initialized, a random color will be generated and stored in the shapes array with this color as the key.
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;
}
Every time you draw on the sceneCanvas, it will also draw it in the hitCanvas in the memory, and use the randomly generated color value as the color of fill and stroke.
When the sceneCanvas is clicked, the clicked coordinate point getImageData
can be obtained by calling 06163313761175 of hitCanvas, and then the corresponding Shape can be found through the colorKey. It is a very clever realization.
But this method is also flawed, because the random hex color generated has an upper limit, there will be up to 256 256 256 = 16777216, if it exceeds this amount, the matching will be inaccurate.
But consider that if there are 16,777,216 DOM nodes, the browser will be stuck. Changing to so many Canvas graphics will also cause a performance explosion.
Custom hitFunc
If you want to customize the event response area, Konva also provides the hitFunc method for you to implement. When drawing the hitCanvas, the original drawing sceneFunc becomes invalid, and instead is drawing the hitFunc.
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);
// ...
}
Drag event
Konva's drag event does not use a native method, but based on mousemove and touchmove to calculate the distance of movement, and then manually set the position of the Shape. The implementation logic is relatively simple, so I won't go into details here.
Filter
Konva supports a variety of filters. Before using the filter, you need to cache the Shape first, and then use the filter()
method to add the filter.
In the cache, in addition to creating a Canvas for off-screen rendering, a filter Canvas is also created. Filter processing is in _getCachedSceneCanvas
.
First draw the sceneCanvas onto the filterCanvas through drawImage, then filterCanvas gets all the ImageData, traverses all the set filter methods, and passes the ImageData to the filter method for processing.
After processing the ImageData, draw it to the filterCanvas through putImageData.
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;
}
How do you draw the filter effect? Special processing is performed in konva. If filterCanvas exists, cacheCanvas will not be used, that is, the off-screen Canvas we originally used for caching will be replaced by filterCanvas.
Finally, the filterCanvas will be drawn on the sceneCanvas by way of drawImage.
Selector
Konva implements a selector so that we can quickly find a certain Shape. There are currently three main types of selectors, namely id selector, name selector, and type selector.
The first two need to pass in an id or name attribute when instantiating, and the latter is searched based on the class name (Rect, Line, etc.).
The find method needs to be called when the selector is searched. This find method is mounted on the Container class. It calls _descendants to traverse the child nodes, and calls the isMatch method of the traversed node nodes to determine whether they match.
_generalFind<ChildNode extends Node = Node>(
selector: string | Function,
findOne: boolean
) {
var retArr: Array<ChildNode> = [];
// 调用 _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;
}
It can be seen in isMatch to perform matching according to the type of selector.
// 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;
}
Serialization
Konva also supports the serialization and deserialization of the Stage. Simply put, it is to export the Stage data into a JSON data and import the JSON data, so that we can perform server-side rendering on the NodeJS side.
Serialization is mainly in the toObject method, which filters functions and DOM nodes, and only retains a description of information, such as Layer information, Shape information, etc., which is a bit similar to the Virtual DOM in React.
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);
}
The deserialization is to parse the incoming JSON information, create different objects according to the className, recurse the deep structure, and then add to the parent node.
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 and React binding does not use the way of repackaged components, but adopts the same form as react-dom and react-native, based on react-reconciler to implement a set of hostConfig, thereby customizing its own Host Component (host component) .
react-reconciler
After the React Fiber architecture was born, they extracted the original React core code. It mainly includes three parts: react, react-reconciler, and platform implementation (react-dom, react-native, etc.).
The famous Diff algorithm, time slicing, scheduling, etc. are implemented in react-reconciler. It also exposes us a hostConfig file, allowing us to implement our own rendering in various hook functions.
In React, there are two types of components, one is Host Component and the other is Composition Component.
In DOM, the former are elements such as h1, div, span, etc. In react-native, the former are elements such as View, Text, and ScrollView. The latter are our custom components based on Host Component, such as App, Header, and so on.
In react-reconciler, it allows us to customize the rendering of the Host Component (addition, deletion, and modification), which also means cross-platform capabilities. We only need to write a hostConfig file to achieve our own rendering.
Refer to the above architecture diagram, you will find that it can be rendered to native, canvas, or even small programs. There are already solutions in the industry based on this. You can refer to Ant Financial's remax: [Remax-Use real React to build small programs
][11]
react-konva
The main implementation of react-konva is in ReactKonvaHostConfig.js. It uses Konva's original API to implement the mapping of Virtual DOM, which responds to the addition, deletion, and modification of Virtual DOM.
Here is part of the source code:
// 创建一个实例
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 实现 insertBefore
export 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
On Vue, Konva registered a plug-in through Vue.use, which registered each component separately.
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);
}
Let's take a look at the implementation of KonvaNode. In KonvaNode, the addition, deletion, and modification of nodes are all implemented in the life cycle of Vue.
In Vue's created life cycle, initKonva is called to new a NodeClass, which is almost the same as React above.
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();
},
While updating the Props when updated, and destroying the node in destroyed, the implementation is more concise.
updated() {
this.uploadKonva();
checkOrder(this.$vnode, this._konvaNode);
},
destroyed() {
updatePicture(this._konvaNode);
this._konvaNode.destroy();
this._konvaNode.off(EVENTS_NAMESPACE);
},
defect
Dirty rectangle
In terms of performance, Konva is still not enough to compare PIXI and ZRender libraries. If we have a lot of Shapes on our Layer, if we want to update a certain Shape, we will still draw all of them according to Konva's implementation.
Although Konva supports redrawing of a single shape, it is not brainless to cover the original position, which also means that if your graph is below other node graphs, there will be problems.
So there is a lack of very important local update capabilities, which is what we often call dirty rectangles.
Dirty rectangle means that when we update a shape, we use collision detection to calculate all shapes that intersect with it, merge them, and calculate a dirty area. Then we use clip to restrict Canvas to draw only in this dirty area, thus realizing partial update.
Unfortunately, the implementation of Konva's bounding box is very simple, not suitable for collision detection, and it does not provide the capability of dirty rectangles.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。