From cbf68b49b25b54d12fb72e353bc192eeeee0cee2 Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 17 Apr 2020 16:37:17 +0800 Subject: [PATCH 01/11] feat(bbox): cache bbox --- packages/g-base/src/abstract/element.ts | 4 +-- packages/g-base/src/abstract/shape.ts | 4 +-- packages/g-canvas/src/canvas.ts | 2 ++ packages/g-canvas/src/group.ts | 35 +++++++++++++++++++++++-- packages/g-canvas/src/shape/base.ts | 10 ++++--- packages/g-canvas/src/util/draw.ts | 2 +- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/g-base/src/abstract/element.ts b/packages/g-base/src/abstract/element.ts index ad8e56509..9b090bdb4 100644 --- a/packages/g-base/src/abstract/element.ts +++ b/packages/g-base/src/abstract/element.ts @@ -294,10 +294,10 @@ abstract class Element extends Base implements IElement { // 获取总的 matrix getTotalMatrix() { - let totalMatrix = this.get('totalMatrix'); + let totalMatrix = this.cfg.totalMatrix; if (!totalMatrix) { const currentMatrix = this.attr('matrix'); - const parentMatrix = this.get('parentMatrix'); + const parentMatrix = this.cfg.parentMatrix; if (parentMatrix && currentMatrix) { totalMatrix = multiplyMatrix(parentMatrix, currentMatrix); } else { diff --git a/packages/g-base/src/abstract/shape.ts b/packages/g-base/src/abstract/shape.ts index acb73dfcc..935a0ad27 100644 --- a/packages/g-base/src/abstract/shape.ts +++ b/packages/g-base/src/abstract/shape.ts @@ -24,7 +24,7 @@ abstract class AbstractShape extends Element implements IShape { } // 计算包围盒时,需要缓存,这是一个高频的操作 getBBox(): BBox { - let bbox = this.get('bbox'); + let bbox = this.cfg.bbox; if (!bbox) { bbox = this.calculateBBox(); this.set('bbox', bbox); @@ -33,7 +33,7 @@ abstract class AbstractShape extends Element implements IShape { } // 计算相对于画布的包围盒 getCanvasBBox(): BBox { - let canvasBox = this.get('canvasBox'); + let canvasBox = this.cfg.canvasBox; if (!canvasBox) { canvasBox = this.calculateCanvasBBox(); this.set('canvasBox', canvasBox); diff --git a/packages/g-canvas/src/canvas.ts b/packages/g-canvas/src/canvas.ts index 9a81a5cf4..82fb7e935 100644 --- a/packages/g-canvas/src/canvas.ts +++ b/packages/g-canvas/src/canvas.ts @@ -183,6 +183,7 @@ class Canvas extends AbstractCanvas { const region = this._getRefreshRegion(); // 需要注意可能没有 region 的场景 // 一般发生在设置了 localRefresh ,在没有图形发生变化的情况下,用户调用了 draw + // const t = performance.now(); if (region) { // 清理指定区域 context.clearRect(region.minX, region.minY, region.maxX - region.minX, region.maxY - region.minY); @@ -196,6 +197,7 @@ class Canvas extends AbstractCanvas { drawChildren(context, children, region); context.restore(); } + // console.log(performance.now() - t); this.set('refreshElements', []); } diff --git a/packages/g-canvas/src/group.ts b/packages/g-canvas/src/group.ts index 57257ed92..07a228b45 100644 --- a/packages/g-canvas/src/group.ts +++ b/packages/g-canvas/src/group.ts @@ -4,8 +4,8 @@ import { IElement } from './interfaces'; import { Region } from './types'; import ShapeBase from './shape/base'; import * as Shape from './shape'; -import { each, mergeRegion } from './util/util'; import { applyAttrsToContext, drawChildren, refreshElement } from './util/draw'; +import { each } from '@antv/util'; class Group extends AbstractGroup { /** @@ -39,6 +39,36 @@ class Group extends AbstractGroup { } } + cacheCanvasBBox() { + const cacheCanvasBBox = this.cfg.cacheCanvasBBox; + if (!cacheCanvasBBox) { + const children = this.getChildren(); + const xArr = []; + const yArr = []; + each(children, (child) => { + const bbox = child.cfg.cacheCanvasBBox; + if (bbox) { + xArr.push(bbox.minX, bbox.maxX); + yArr.push(bbox.minY, bbox.maxY); + } + }); + const minX = Math.min.apply(null, xArr); + const maxX = Math.max.apply(null, xArr); + const minY = Math.min.apply(null, yArr); + const maxY = Math.max.apply(null, yArr); + this.set('cacheCanvasBBox', { + minX, + minY, + x: minX, + y: minY, + maxX, + maxY, + width: maxX - minX, + height: maxY - minY, + }); + } + } + draw(context: CanvasRenderingContext2D, region?: Region) { const children = this.getChildren() as IElement[]; if (children.length) { @@ -51,7 +81,8 @@ class Group extends AbstractGroup { context.restore(); } // 这里的成本比较大 - this.set('cacheCanvasBBox', this.getCanvasBBox()); + // this.set('cacheCanvasBBox', this.getCanvasBBox()); + this.cacheCanvasBBox(); // 绘制后,消除更新标记 this.set('hasChanged', false); } diff --git a/packages/g-canvas/src/shape/base.ts b/packages/g-canvas/src/shape/base.ts index 0bb260c76..82d70cb19 100644 --- a/packages/g-canvas/src/shape/base.ts +++ b/packages/g-canvas/src/shape/base.ts @@ -84,7 +84,7 @@ class ShapeBase extends AbstractShape { // 绘制图形时需要考虑 region 限制 draw(context: CanvasRenderingContext2D, region?: Region) { - const clip = this.getClip(); + const clip = this.cfg.clipShape; // 如果指定了区域,当与指定区域相交时,才会触发渲染 if (region) { // 是否相交需要考虑 clip 的包围盒 @@ -96,13 +96,13 @@ class ShapeBase extends AbstractShape { context.save(); // 先将 attrs 应用到上下文中,再设置 clip。因为 clip 应该被当前元素的 matrix 所影响 applyAttrsToContext(context, this); - this._applyClip(context, this.getClip() as ShapeBase); + this._applyClip(context, clip as ShapeBase); this.drawPath(context); context.restore(); this._afterDraw(); } - _afterDraw() { + cacheCanvasBBox() { const bbox = this.getCanvasBBox(); const canvas = this.getCanvas(); // 绘制的时候缓存包围盒 @@ -112,6 +112,10 @@ class ShapeBase extends AbstractShape { const viewRange = canvas.getViewRange(); this.set('isInView', intersectRect(bbox, viewRange)); } + } + + _afterDraw() { + this.cacheCanvasBBox(); // 绘制后消除标记 this.set('hasChanged', false); } diff --git a/packages/g-canvas/src/util/draw.ts b/packages/g-canvas/src/util/draw.ts index 8ef231814..06be3389e 100644 --- a/packages/g-canvas/src/util/draw.ts +++ b/packages/g-canvas/src/util/draw.ts @@ -41,7 +41,7 @@ export function applyAttrsToContext(context: CanvasRenderingContext2D, element: export function drawChildren(context: CanvasRenderingContext2D, children: IElement[], region?: Region) { for (let i = 0; i < children.length; i++) { const child = children[i] as IElement; - if (child.get('visible')) { + if (child.cfg.visible) { child.draw(context, region); } else { child.skipDraw(); From 0e3100269a2a3d93d93d6ca53c8864e60ec68a84 Mon Sep 17 00:00:00 2001 From: dxq613 Date: Wed, 22 Apr 2020 19:33:48 +0800 Subject: [PATCH 02/11] feat(rename): change canvasBox to canvasBBox --- packages/g-base/src/abstract/shape.ts | 14 +++++++------- packages/g-base/tests/unit/bbox-spec.js | 2 +- packages/g-base/tests/unit/group-spec.js | 4 ++-- packages/g-base/tests/unit/shape-spec.js | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/g-base/src/abstract/shape.ts b/packages/g-base/src/abstract/shape.ts index 935a0ad27..d688f1ccc 100644 --- a/packages/g-base/src/abstract/shape.ts +++ b/packages/g-base/src/abstract/shape.ts @@ -33,12 +33,12 @@ abstract class AbstractShape extends Element implements IShape { } // 计算相对于画布的包围盒 getCanvasBBox(): BBox { - let canvasBox = this.cfg.canvasBox; - if (!canvasBox) { - canvasBox = this.calculateCanvasBBox(); - this.set('canvasBox', canvasBox); + let canvasBBox = this.cfg.canvasBBox; + if (!canvasBBox) { + canvasBBox = this.calculateCanvasBBox(); + this.set('canvasBBox', canvasBBox); } - return canvasBox; + return canvasBBox; } /** @@ -50,7 +50,7 @@ abstract class AbstractShape extends Element implements IShape { applyMatrix(matrix: number[]) { super.applyMatrix(matrix); // 清理掉缓存的包围盒 - this.set('canvasBox', null); + this.set('canvasBBox', null); } /** @@ -102,7 +102,7 @@ abstract class AbstractShape extends Element implements IShape { */ clearCacheBBox() { this.set('bbox', null); - this.set('canvasBox', null); + this.set('canvasBBox', null); } // 实现接口 diff --git a/packages/g-base/tests/unit/bbox-spec.js b/packages/g-base/tests/unit/bbox-spec.js index 445728c48..ecb9740e4 100644 --- a/packages/g-base/tests/unit/bbox-spec.js +++ b/packages/g-base/tests/unit/bbox-spec.js @@ -283,7 +283,7 @@ describe('test bbox', () => { expect(canvasBBox.y).eql(bbox.y - 20); rect.set('totalMatrix', [1, 0, 0, 1, 0, 0, 10, 20, 1]); // 位移 10, 20 - rect.set('canvasBox', null); + rect.set('canvasBBox', null); canvasBBox = rect.getCanvasBBox(); expect(canvasBBox.x).eql(bbox.x + 10); diff --git a/packages/g-base/tests/unit/group-spec.js b/packages/g-base/tests/unit/group-spec.js index d4d8d0666..623daa6fc 100644 --- a/packages/g-base/tests/unit/group-spec.js +++ b/packages/g-base/tests/unit/group-spec.js @@ -91,8 +91,8 @@ describe('test group', () => { expect(bbox.minY).eqls(-20); expect(bbox.maxX).eqls(30); expect(bbox.maxY).eqls(30); - const canvasBox = group.getCanvasBBox(); - expect(canvasBox).eqls(bbox); + const canvasBBox = group.getCanvasBBox(); + expect(canvasBBox).eqls(bbox); }); it('remove shape', () => { diff --git a/packages/g-base/tests/unit/shape-spec.js b/packages/g-base/tests/unit/shape-spec.js index bf6ae9bd2..5a76acad6 100644 --- a/packages/g-base/tests/unit/shape-spec.js +++ b/packages/g-base/tests/unit/shape-spec.js @@ -67,15 +67,15 @@ describe('test element', () => { expect(bbox.maxX).equal(100 * 2); expect(bbox.maxY).equal(100 * 2); expect(shape.getCanvasBBox()).eql(bbox); // 测试缓存 - expect(shape.get('canvasBox')).not.eqls(undefined); - expect(shape.get('canvasBox')).not.eqls(null); + expect(shape.get('canvasBBox')).not.eqls(undefined); + expect(shape.get('canvasBBox')).not.eqls(null); }); it('attr change', () => { shape.attr('x', 10); shape.attr('y', 10); expect(shape.get('bbox')).eqls(null); - expect(shape.get('canvasBox')).eqls(null); + expect(shape.get('canvasBBox')).eqls(null); expect(shape.getBBox()).eqls({ minX: 10, minY: 10, From b35c90f7a269e7d310cfdeff33638bb46780e532 Mon Sep 17 00:00:00 2001 From: dxq613 Date: Wed, 22 Apr 2020 19:34:56 +0800 Subject: [PATCH 03/11] feat(render): prompt refresh --- packages/g-canvas/src/canvas.ts | 6 +- packages/g-canvas/src/group.ts | 59 +++++++------- packages/g-canvas/src/shape/base.ts | 10 ++- packages/g-canvas/src/util/draw.ts | 80 ++++++++++++++++++- .../g-canvas/tests/unit/canvas-draw-spec.js | 8 +- .../tests/unit/util/quick-hit-spec.js | 18 +++-- 6 files changed, 130 insertions(+), 51 deletions(-) diff --git a/packages/g-canvas/src/canvas.ts b/packages/g-canvas/src/canvas.ts index 82fb7e935..a3509f74a 100644 --- a/packages/g-canvas/src/canvas.ts +++ b/packages/g-canvas/src/canvas.ts @@ -5,7 +5,7 @@ import { getShape } from './util/hit'; import EventController from '@antv/g-base/lib/event/event-contoller'; import * as Shape from './shape'; import Group from './group'; -import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView } from './util/draw'; +import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView, checkRefresh } from './util/draw'; import { getPixelRatio, requestAnimationFrame, clearAnimationFrame } from './util/util'; class Canvas extends AbstractCanvas { @@ -183,7 +183,6 @@ class Canvas extends AbstractCanvas { const region = this._getRefreshRegion(); // 需要注意可能没有 region 的场景 // 一般发生在设置了 localRefresh ,在没有图形发生变化的情况下,用户调用了 draw - // const t = performance.now(); if (region) { // 清理指定区域 context.clearRect(region.minX, region.minY, region.maxX - region.minX, region.maxY - region.minY); @@ -193,11 +192,12 @@ class Canvas extends AbstractCanvas { context.rect(region.minX, region.minY, region.maxX - region.minX, region.maxY - region.minY); context.clip(); applyAttrsToContext(context, this); + // 确认更新的元素,这个优化可以提升 10 倍左右的性能,10W 个带有 group 的节点,局部渲染会从 90ms 下降到 5-6 ms + checkRefresh(this, children, region); // 绘制子元素 drawChildren(context, children, region); context.restore(); } - // console.log(performance.now() - t); this.set('refreshElements', []); } diff --git a/packages/g-canvas/src/group.ts b/packages/g-canvas/src/group.ts index 07a228b45..d040eb0c1 100644 --- a/packages/g-canvas/src/group.ts +++ b/packages/g-canvas/src/group.ts @@ -40,38 +40,36 @@ class Group extends AbstractGroup { } cacheCanvasBBox() { - const cacheCanvasBBox = this.cfg.cacheCanvasBBox; - if (!cacheCanvasBBox) { - const children = this.getChildren(); - const xArr = []; - const yArr = []; - each(children, (child) => { - const bbox = child.cfg.cacheCanvasBBox; - if (bbox) { - xArr.push(bbox.minX, bbox.maxX); - yArr.push(bbox.minY, bbox.maxY); - } - }); - const minX = Math.min.apply(null, xArr); - const maxX = Math.max.apply(null, xArr); - const minY = Math.min.apply(null, yArr); - const maxY = Math.max.apply(null, yArr); - this.set('cacheCanvasBBox', { - minX, - minY, - x: minX, - y: minY, - maxX, - maxY, - width: maxX - minX, - height: maxY - minY, - }); - } + const children = this.getChildren(); + const xArr = []; + const yArr = []; + each(children, (child) => { + const bbox = child.cfg.cacheCanvasBBox; + if (bbox) { + xArr.push(bbox.minX, bbox.maxX); + yArr.push(bbox.minY, bbox.maxY); + } + }); + const minX = Math.min.apply(null, xArr); + const maxX = Math.max.apply(null, xArr); + const minY = Math.min.apply(null, yArr); + const maxY = Math.max.apply(null, yArr); + this.set('cacheCanvasBBox', { + minX, + minY, + x: minX, + y: minY, + maxX, + maxY, + width: maxX - minX, + height: maxY - minY, + }); } draw(context: CanvasRenderingContext2D, region?: Region) { const children = this.getChildren() as IElement[]; - if (children.length) { + const allowDraw = region ? this.cfg.refresh : true; // 局部刷新需要判定 + if (children.length && allowDraw) { context.save(); // group 上的矩阵和属性也会应用到上下文上 // 先将 attrs 应用到上下文中,再设置 clip。因为 clip 应该被当前元素的 matrix 所影响 @@ -79,10 +77,11 @@ class Group extends AbstractGroup { this._applyClip(context, this.getClip() as ShapeBase); drawChildren(context, children, region); context.restore(); + this.cacheCanvasBBox(); } - // 这里的成本比较大 + // 这里的成本比较大,如果不绘制则不再 // this.set('cacheCanvasBBox', this.getCanvasBBox()); - this.cacheCanvasBBox(); + this.cfg.refresh = null; // 绘制后,消除更新标记 this.set('hasChanged', false); } diff --git a/packages/g-canvas/src/shape/base.ts b/packages/g-canvas/src/shape/base.ts index 82d70cb19..6ed859f18 100644 --- a/packages/g-canvas/src/shape/base.ts +++ b/packages/g-canvas/src/shape/base.ts @@ -85,11 +85,16 @@ class ShapeBase extends AbstractShape { // 绘制图形时需要考虑 region 限制 draw(context: CanvasRenderingContext2D, region?: Region) { const clip = this.cfg.clipShape; - // 如果指定了区域,当与指定区域相交时,才会触发渲染 + // 如果指定了 region,同时不允许刷新时,直接返回 if (region) { + if (this.cfg.refresh === false) { + this.set('hasChanged', false); + return; + } // 是否相交需要考虑 clip 的包围盒 - const bbox = clip ? getMergedRegion([this, clip]) : this.getCanvasBBox(); + const bbox = this.getCanvasBBox(); if (!intersectRect(region, bbox)) { + this.set('hasChanged', false); return; } } @@ -118,6 +123,7 @@ class ShapeBase extends AbstractShape { this.cacheCanvasBBox(); // 绘制后消除标记 this.set('hasChanged', false); + this.set('refresh', null); } skipDraw() { diff --git a/packages/g-canvas/src/util/draw.ts b/packages/g-canvas/src/util/draw.ts index 06be3389e..a6ebec4c3 100644 --- a/packages/g-canvas/src/util/draw.ts +++ b/packages/g-canvas/src/util/draw.ts @@ -1,5 +1,5 @@ import { each, isArray } from '@antv/util'; -import { IElement } from '../interfaces'; +import { IElement, IGroup } from '../interfaces'; import { Region } from '../types'; import { parseStyle } from './parse'; import getArcParams from './arc-params'; @@ -49,6 +49,74 @@ export function drawChildren(context: CanvasRenderingContext2D, children: IEleme } } +export function checkRefresh(canvas, children: IElement[], region: Region) { + const refreshElements = canvas.get('refreshElements'); + // 先遍历需要刷新的元素,将这些元素的父元素也设置 refresh + each(refreshElements, (el) => { + if (el !== canvas) { + let parent = el.cfg.parent; + while (parent && parent !== canvas && !parent.cfg.refresh) { + parent.cfg.refresh = true; + parent = parent.cfg.parent; + } + } + }); + if (refreshElements[0] === canvas) { + setChildrenRefresh(children, region); + } else { + // 检查所有子元素是否可以刷新 + checkChildrenRefresh(children, region); + } +} +// 检查所有的子元素是否应该更新 +export function checkChildrenRefresh(children: IElement[], region: Region) { + for (let i = 0; i < children.length; i++) { + const child = children[i] as IElement; + if (child.cfg.visible) { + // 如果当前图形/分组 refresh = true,说明其子节点存在 changed + if (child.cfg.refresh) { + if (child.isGroup()) { + checkChildrenRefresh(child.get('children'), region); + } + } else if (child.cfg.hasChanged) { + // 如果节点发生了 change,则需要级联设置子元素的 refresh + child.cfg.refresh = true; + if (child.isGroup()) { + setChildrenRefresh(child.get('children'), region); + } + } else { + // 这个分支说明此次局部刷新,所有的节点和父元素没有发生变化,仅需要检查包围盒(缓存)是否相交即可 + child.cfg.refresh = checkElementRefresh(child, region); + } + } + } +} + +// 当某个父元素发生改变时,调用这个方法级联设置 refresh +function setChildrenRefresh(children: IElement[], region: Region) { + for (let i = 0; i < children.length; i++) { + const child = children[i] as IElement; + let refresh = true; + // 获取缓存的 bbox,如果这个 bbox 还存在则说明父元素不是矩阵发生了改变 + const bbox = child.cfg.canvasBBox; + if (bbox) { + // 如果这时候 + refresh = intersectRect(bbox, region); + } + child.cfg.refresh = refresh; + // 如果需要刷新当前节点,所有的子元素设置 refresh + if (refresh && child.isGroup()) { + setChildrenRefresh(child.get('children'), region); + } + } +} + +function checkElementRefresh(shape: IElement, region: Region): boolean { + const bbox = shape.cfg.cacheCanvasBBox; + const isAllow = bbox && intersectRect(bbox, region); + return isAllow; +} + // 绘制 path export function drawPath(shape, context, attrs, arcParamsCache) { const { path, startArrow, endArrow } = attrs; @@ -160,10 +228,14 @@ export function refreshElement(element, changeType) { // canvas.refreshElement(element, changeType, canvas); // } // 但对于 https://github.com/antvis/g/issues/422 的场景,全局渲染的模式下也需要记录更新的元素队列 - canvas.refreshElement(element, changeType, canvas); - if (canvas.get('autoDraw')) { - canvas.draw(); + // 如果当前元素的父元素发生了改变,可以不放入队列,这句话大概能够提升 15% 的初次渲染性能 + if (!(element.cfg.parent && element.cfg.parent.get('hasChanged'))) { + canvas.refreshElement(element, changeType, canvas); + if (canvas.get('autoDraw')) { + canvas.draw(); + } } + // 但是始终要标记为 hasChanged,便于后面进行局部渲染 element.set('hasChanged', true); } } diff --git a/packages/g-canvas/tests/unit/canvas-draw-spec.js b/packages/g-canvas/tests/unit/canvas-draw-spec.js index e89660334..5b44702dc 100644 --- a/packages/g-canvas/tests/unit/canvas-draw-spec.js +++ b/packages/g-canvas/tests/unit/canvas-draw-spec.js @@ -3,7 +3,7 @@ import Canvas from '../../src/canvas'; import Group from '../../src/group'; import { getColor } from '../get-color'; -const DELAY = 25; // 本来应该 16ms,但是测试时需要适当调大这个值 +const DELAY = 30; // 本来应该 16ms,但是测试时需要适当调大这个值 const dom = document.createElement('div'); document.body.appendChild(dom); @@ -301,11 +301,11 @@ describe('test canvas draw', () => { x: 60, y: 60, r: 5, - fill: '#000fff', + fill: '#00ffff', }, }); setTimeout(() => { - expect(getColor(context, 60, 60)).eql('#000fff'); + expect(getColor(context, 60, 60)).eql('#00ffff'); done(); }, DELAY); }); @@ -316,7 +316,7 @@ describe('test canvas draw', () => { group1.setMatrix(matrix); setTimeout(() => { expect(getColor(context, 60, 60)).eql('#000000'); - expect(getColor(context, 80, 60)).eql('#000fff'); + expect(getColor(context, 80, 60)).eql('#00ffff'); done(); }, DELAY); }); diff --git a/packages/g-canvas/tests/unit/util/quick-hit-spec.js b/packages/g-canvas/tests/unit/util/quick-hit-spec.js index 82abf6e78..727fa7959 100644 --- a/packages/g-canvas/tests/unit/util/quick-hit-spec.js +++ b/packages/g-canvas/tests/unit/util/quick-hit-spec.js @@ -28,9 +28,11 @@ describe('quick hit test', () => { height: maxY, }); + const root = canvas.addGroup(); + xit('no group and all in view', () => { for (let i = 0; i < count; i++) { - canvas.addShape('circle', { + root.addShape('circle', { attrs: { x: Math.random() * maxX, y: Math.random() * maxY, @@ -55,9 +57,9 @@ describe('quick hit test', () => { }); xit('no group some out view', (done) => { - canvas.clear(); + root.clear(); for (let i = 0; i < count; i++) { - canvas.addShape('circle', { + root.addShape('circle', { attrs: { x: Math.random() * maxX * 2, y: Math.random() * maxY * 2, @@ -84,14 +86,14 @@ describe('quick hit test', () => { }); xit('with group and matrix', (done) => { - canvas.clear(); + root.clear(); for (let i = 0; i < 10; i++) { - const group = canvas.addGroup(); + const group = root.addGroup(); group.translate(100 * Math.random(), 100 * Math.random()); } for (let i = 0; i < count; i++) { const index = i % 10; - const group = canvas.getChildByIndex(index); + const group = root.getChildByIndex(index); group.addShape('circle', { attrs: { x: Math.random() * maxX * 2, @@ -127,9 +129,9 @@ describe('quick hit test', () => { ]; } xit('more groups', (done) => { - canvas.clear(); + root.clear(); for (let i = 0; i < count * 2; i++) { - const group = canvas.addGroup(); + const group = root.addGroup(); group.translate(100 * Math.random(), 100 * Math.random()); const x = Math.random() * maxX * 4 - 100; const y = Math.random() * maxY * 4 - 100; From ecf7d617054d34af79e67fca170b470685ce686d Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 16:50:31 +0800 Subject: [PATCH 04/11] feat(group): add group in trigger onCanvasChanged --- packages/g-base/src/abstract/container.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/g-base/src/abstract/container.ts b/packages/g-base/src/abstract/container.ts index a0dde84db..67174626f 100644 --- a/packages/g-base/src/abstract/container.ts +++ b/packages/g-base/src/abstract/container.ts @@ -7,16 +7,6 @@ import { isFunction, isObject, each, removeFromArray, upperFirst, isAllowCapture const SHAPE_MAP = {}; const INDEX = '_INDEX'; -function afterAdd(element: IElement) { - if (element.isGroup()) { - if ((element as IGroup).isEntityGroup() || element.get('children').length) { - element.onCanvasChange('add'); - } - } else { - element.onCanvasChange('add'); - } -} - /** * 设置 canvas * @param {IElement} element 元素 @@ -305,7 +295,7 @@ abstract class Container extends Element implements IContainer { setTimeline(element, timeline); } children.push(element); - afterAdd(element); + element.onCanvasChange('add'); this._applyElementMatrix(element); } From 30f328f08e810cb529ea8ecc509f2d200bed0fc2 Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 16:52:53 +0800 Subject: [PATCH 05/11] feat(clip): clip changed --- packages/g-base/src/abstract/element.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/g-base/src/abstract/element.ts b/packages/g-base/src/abstract/element.ts index 9b090bdb4..d9de0be42 100644 --- a/packages/g-base/src/abstract/element.ts +++ b/packages/g-base/src/abstract/element.ts @@ -1,5 +1,5 @@ import { each, isEqual, isFunction, isNumber, isObject, isArray, noop, mix, upperFirst, uniqueId } from '@antv/util'; -import { transform } from '@antv/matrix-util'; +import { transform } from '@antv/matrix-util/lib/mat3'; import { IElement, IShape, IGroup, ICanvas, ICtor } from '../interfaces'; import { ClipCfg, ChangeType, OnFrame, ShapeAttrs, AnimateCfg, Animation, BBox, ShapeBase } from '../types'; import { removeFromArray, isParent } from '../util/util'; @@ -209,7 +209,14 @@ abstract class Element extends Base implements IElement { * @protected */ afterAttrsChange(targetAttrs) { - this.onCanvasChange('attr'); + if (this.cfg.isClipShape) { + const applyTo = this.cfg.applyTo; + if (applyTo) { + applyTo.onCanvasChange('clip'); + } + } else { + this.onCanvasChange('attr'); + } } show() { @@ -371,6 +378,7 @@ abstract class Element extends Base implements IElement { clipShape = new Cons({ type: clipCfg.type, isClipShape: true, // 增加一个标记 + applyTo: this, attrs: clipCfg.attrs, canvas, // 设置 canvas }); From 2c9ade56682748567a0be92905644f89af07f43e Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 17:00:02 +0800 Subject: [PATCH 06/11] feat(matrix-util): update matrix-util and update version --- packages/g-base/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/g-base/package.json b/packages/g-base/package.json index 47f51bb25..1283f80b1 100644 --- a/packages/g-base/package.json +++ b/packages/g-base/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g-base", - "version": "0.4.4", + "version": "0.5.0", "description": "A common util collection for antv projects", "main": "lib/index.js", "module": "esm/index.js", @@ -22,7 +22,7 @@ "coverage-generator": "torch --coverage --compile --source-pattern src/*.js,src/**/*.js --opts tests/mocha.opts", "coverage-viewer": "torch-coverage", "test": "torch --renderer --compile --opts tests/mocha.opts", - "test-live": "torch --compile --interactive --opts tests/mocha.opts", + "test-live": "torch --compile --interactive tests/unit", "tsc": "tsc --noEmit", "typecheck": "tsc --noEmit" }, @@ -54,7 +54,7 @@ "dependencies": { "@antv/event-emitter": "^0.1.1", "@antv/g-math": "^0.1.3", - "@antv/matrix-util": "^2.0.4", + "@antv/matrix-util": "^3.0.1-beta.1", "@antv/path-util": "~2.0.5", "@antv/util": "~2.0.0", "@types/d3-timer": "^1.0.9", From aa1630f3247f63c8d67b8ea8cc63461fce6ba843 Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 17:46:35 +0800 Subject: [PATCH 07/11] feat(matrix): replace matrix-util --- packages/g-canvas/src/util/path.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/g-canvas/src/util/path.ts b/packages/g-canvas/src/util/path.ts index 6571aa3b6..7e9ded5f9 100644 --- a/packages/g-canvas/src/util/path.ts +++ b/packages/g-canvas/src/util/path.ts @@ -3,15 +3,13 @@ * @author dxq613@gmail.com */ import { PathUtil } from '@antv/g-base'; -import getArcParams from './arc-params'; import QuadUtil from '@antv/g-math/lib/quadratic'; import CubicUtil from '@antv/g-math/lib/cubic'; -import EllipseArcUtil from '@antv/g-math/lib/arc'; -import { inBox, isSamePoint } from './util'; +import { inBox } from './util'; import inLine from './in-stroke/line'; import inArc from './in-stroke/arc'; -import * as mat3 from 'gl-matrix/mat3'; +import { transform } from '@antv/matrix-util/lib/mat3'; import * as vec3 from 'gl-matrix/vec3'; function hasArc(path) { @@ -77,13 +75,14 @@ function isPointInStroke(segments, lineWidth, x, y) { const arcParams = segment.arcParams; const { cx, cy, rx, ry, startAngle, endAngle, xRotation } = arcParams; const p = [x, y, 1]; - const m = [1, 0, 0, 0, 1, 0, 0, 0, 1]; const r = rx > ry ? rx : ry; const scaleX = rx > ry ? 1 : rx / ry; const scaleY = rx > ry ? ry / rx : 1; - mat3.translate(m, m, [-cx, -cy]); - mat3.rotate(m, m, -xRotation); - mat3.scale(m, m, [1 / scaleX, 1 / scaleY]); + const m = transform(null, [ + ['t', -cx, -cy], + ['r', -xRotation], + ['s', 1 / scaleX, 1 / scaleY], + ]); vec3.transformMat3(p, p, m); isHit = inArc(0, 0, r, startAngle, endAngle, lineWidth, p[0], p[1]); break; From bbe2c4d452444f38e518ef8352211689d2f899ff Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 17:48:38 +0800 Subject: [PATCH 08/11] feat(refresh): prompt hit and refresh --- packages/g-canvas/package.json | 7 +- packages/g-canvas/src/canvas.ts | 21 ++- packages/g-canvas/src/group.ts | 47 +++-- packages/g-canvas/src/shape/base.ts | 32 +++- packages/g-canvas/src/util/draw.ts | 40 +++-- packages/g-canvas/src/util/hit.ts | 12 +- .../g-canvas/tests/unit/canvas-draw-spec.js | 7 +- .../tests/unit/canvas-refresh-spec.js | 169 ++++++++++++++++++ .../tests/unit/util/quick-hit-spec.js | 22 ++- 9 files changed, 291 insertions(+), 66 deletions(-) create mode 100644 packages/g-canvas/tests/unit/canvas-refresh-spec.js diff --git a/packages/g-canvas/package.json b/packages/g-canvas/package.json index 79c09238e..720af9809 100644 --- a/packages/g-canvas/package.json +++ b/packages/g-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g-canvas", - "version": "0.4.9", + "version": "0.5.0", "description": "A canvas library which providing 2d", "main": "lib/index.js", "module": "esm/index.js", @@ -24,7 +24,7 @@ "coverage-generator": "torch --coverage --compile --source-pattern src/*.js,src/**/*.js --opts tests/mocha.opts", "coverage-viewer": "torch-coverage", "test": "torch --renderer --compile --opts tests/mocha.opts", - "test-live": "torch --compile --interactive --opts tests/mocha.opts", + "test-live": "torch --compile --interactive tests/unit/", "tsc": "tsc --noEmit", "typecheck": "tsc --noEmit", "dist": "webpack --config webpack.config.js --mode production" @@ -55,10 +55,11 @@ }, "homepage": "https://github.com/antvis/g#readme", "dependencies": { - "@antv/g-base": "^0.4.4", + "@antv/g-base": "^0.5.0", "@antv/g-math": "^0.1.3", "@antv/path-util": "~2.0.5", "@antv/util": "~2.0.0", + "@antv/matrix-util": "^3.0.2", "gl-matrix": "^3.0.0" }, "__npminstall_done": false diff --git a/packages/g-canvas/src/canvas.ts b/packages/g-canvas/src/canvas.ts index a3509f74a..2e8947848 100644 --- a/packages/g-canvas/src/canvas.ts +++ b/packages/g-canvas/src/canvas.ts @@ -5,7 +5,7 @@ import { getShape } from './util/hit'; import EventController from '@antv/g-base/lib/event/event-contoller'; import * as Shape from './shape'; import Group from './group'; -import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView, checkRefresh } from './util/draw'; +import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView, checkRefresh, clearChanged } from './util/draw'; import { getPixelRatio, requestAnimationFrame, clearAnimationFrame } from './util/util'; class Canvas extends AbstractCanvas { @@ -61,8 +61,8 @@ class Canvas extends AbstractCanvas { return { minX: 0, minY: 0, - maxX: this.get('width'), - maxY: this.get('height'), + maxX: this.cfg.width, + maxY: this.cfg.height, }; } @@ -105,10 +105,13 @@ class Canvas extends AbstractCanvas { } getShape(x: number, y: number) { + let shape; if (this.get('quickHit')) { - return getShape(this, x, y); + shape = getShape(this, x, y); + } else { + shape = super.getShape(x, y, null); } - return super.getShape(x, y, null); + return shape; } // 对绘制区域边缘取整,避免浮点数问题 _getRefreshRegion() { @@ -181,6 +184,7 @@ class Canvas extends AbstractCanvas { const context = this.get('context'); const children = this.getChildren() as IElement[]; const region = this._getRefreshRegion(); + const refreshElements = this.get('refreshElements'); // 需要注意可能没有 region 的场景 // 一般发生在设置了 localRefresh ,在没有图形发生变化的情况下,用户调用了 draw if (region) { @@ -197,6 +201,13 @@ class Canvas extends AbstractCanvas { // 绘制子元素 drawChildren(context, children, region); context.restore(); + } else if (refreshElements.length) { + // 防止发生改变的 elements 没有 region 的场景,这会发生在多个情况下 + // 1. 空的 group + // 2. 所有 elements 没有在绘图区域 + // 3. group 下面的 elements 隐藏掉 + // 如果不进行清理 hasChanged 的状态会不正确 + clearChanged(refreshElements); } this.set('refreshElements', []); } diff --git a/packages/g-canvas/src/group.ts b/packages/g-canvas/src/group.ts index d040eb0c1..31b421694 100644 --- a/packages/g-canvas/src/group.ts +++ b/packages/g-canvas/src/group.ts @@ -6,6 +6,7 @@ import ShapeBase from './shape/base'; import * as Shape from './shape'; import { applyAttrsToContext, drawChildren, refreshElement } from './util/draw'; import { each } from '@antv/util'; +import { intersectRect } from './util/util'; class Group extends AbstractGroup { /** @@ -40,34 +41,46 @@ class Group extends AbstractGroup { } cacheCanvasBBox() { - const children = this.getChildren(); + const children = this.cfg.children; const xArr = []; const yArr = []; each(children, (child) => { const bbox = child.cfg.cacheCanvasBBox; - if (bbox) { + if (bbox && child.cfg.isInView) { xArr.push(bbox.minX, bbox.maxX); yArr.push(bbox.minY, bbox.maxY); } }); - const minX = Math.min.apply(null, xArr); - const maxX = Math.max.apply(null, xArr); - const minY = Math.min.apply(null, yArr); - const maxY = Math.max.apply(null, yArr); - this.set('cacheCanvasBBox', { - minX, - minY, - x: minX, - y: minY, - maxX, - maxY, - width: maxX - minX, - height: maxY - minY, - }); + let bbox = null; + if (xArr.length) { + const minX = Math.min.apply(null, xArr); + const maxX = Math.max.apply(null, xArr); + const minY = Math.min.apply(null, yArr); + const maxY = Math.max.apply(null, yArr); + bbox = { + minX, + minY, + x: minX, + y: minY, + maxX, + maxY, + width: maxX - minX, + height: maxY - minY, + }; + const canvas = this.cfg.canvas; + if (canvas) { + const viewRange = canvas.getViewRange(); + this.set('isInView', intersectRect(bbox, viewRange)); + } + } else { + this.set('isInView', false); + } + + this.set('cacheCanvasBBox', bbox); } draw(context: CanvasRenderingContext2D, region?: Region) { - const children = this.getChildren() as IElement[]; + const children = this.cfg.children as IElement[]; const allowDraw = region ? this.cfg.refresh : true; // 局部刷新需要判定 if (children.length && allowDraw) { context.save(); diff --git a/packages/g-canvas/src/shape/base.ts b/packages/g-canvas/src/shape/base.ts index 6ed859f18..9f44708b3 100644 --- a/packages/g-canvas/src/shape/base.ts +++ b/packages/g-canvas/src/shape/base.ts @@ -1,7 +1,7 @@ import { AbstractShape } from '@antv/g-base'; import { ChangeType, BBox } from '@antv/g-base/lib/types'; import { isNil, intersectRect } from '../util/util'; -import { applyAttrsToContext, refreshElement, getMergedRegion } from '../util/draw'; +import { applyAttrsToContext, refreshElement } from '../util/draw'; import { getBBoxMethod } from '@antv/g-base/lib/bbox/index'; import { Region } from '../types'; import * as Shape from './index'; @@ -88,6 +88,7 @@ class ShapeBase extends AbstractShape { // 如果指定了 region,同时不允许刷新时,直接返回 if (region) { if (this.cfg.refresh === false) { + // this._afterDraw(); this.set('hasChanged', false); return; } @@ -95,6 +96,9 @@ class ShapeBase extends AbstractShape { const bbox = this.getCanvasBBox(); if (!intersectRect(region, bbox)) { this.set('hasChanged', false); + if (this.cfg.isInView) { + this._afterDraw(); + } return; } } @@ -107,15 +111,27 @@ class ShapeBase extends AbstractShape { this._afterDraw(); } - cacheCanvasBBox() { - const bbox = this.getCanvasBBox(); - const canvas = this.getCanvas(); - // 绘制的时候缓存包围盒 - this.set('cacheCanvasBBox', bbox); + private getCanvasViewBox() { + const canvas = this.cfg.canvas; if (canvas) { // @ts-ignore - const viewRange = canvas.getViewRange(); - this.set('isInView', intersectRect(bbox, viewRange)); + return canvas.getViewRange(); + } + return null; + } + + cacheCanvasBBox() { + const canvasBBox = this.getCanvasViewBox(); + // 绘制的时候缓存包围盒 + if (canvasBBox) { + const bbox = this.getCanvasBBox(); + const isInView = intersectRect(bbox, canvasBBox); + this.set('isInView', isInView); + if (isInView) { + this.set('cacheCanvasBBox', bbox); + } else { + this.set('cacheCanvasBBox', null); + } } } diff --git a/packages/g-canvas/src/util/draw.ts b/packages/g-canvas/src/util/draw.ts index a6ebec4c3..5f5c72ebb 100644 --- a/packages/g-canvas/src/util/draw.ts +++ b/packages/g-canvas/src/util/draw.ts @@ -76,13 +76,13 @@ export function checkChildrenRefresh(children: IElement[], region: Region) { // 如果当前图形/分组 refresh = true,说明其子节点存在 changed if (child.cfg.refresh) { if (child.isGroup()) { - checkChildrenRefresh(child.get('children'), region); + checkChildrenRefresh(child.cfg.children, region); } } else if (child.cfg.hasChanged) { // 如果节点发生了 change,则需要级联设置子元素的 refresh child.cfg.refresh = true; if (child.isGroup()) { - setChildrenRefresh(child.get('children'), region); + setChildrenRefresh(child.cfg.children, region); } } else { // 这个分支说明此次局部刷新,所有的节点和父元素没有发生变化,仅需要检查包围盒(缓存)是否相交即可 @@ -92,20 +92,33 @@ export function checkChildrenRefresh(children: IElement[], region: Region) { } } +// 由于对改变的图形放入 refreshElements 时做了优化,判定父元素 changed 时不加入 +// 那么有可能会出现 elements 都为空,所以最终 group +export function clearChanged(elements: IElement[]) { + for (let i = 0; i < elements.length; i++) { + const el = elements[i]; + el.cfg.hasChanged = false; + // 级联清理 + if (el.isGroup()) { + clearChanged(el.cfg.children); + } + } +} + // 当某个父元素发生改变时,调用这个方法级联设置 refresh function setChildrenRefresh(children: IElement[], region: Region) { for (let i = 0; i < children.length; i++) { const child = children[i] as IElement; - let refresh = true; + // let refresh = true; // 获取缓存的 bbox,如果这个 bbox 还存在则说明父元素不是矩阵发生了改变 - const bbox = child.cfg.canvasBBox; - if (bbox) { - // 如果这时候 - refresh = intersectRect(bbox, region); - } - child.cfg.refresh = refresh; + // const bbox = child.cfg.canvasBBox; + // if (bbox) { + // // 如果这时候 + // refresh = intersectRect(bbox, region); + // } + child.cfg.refresh = true; // 如果需要刷新当前节点,所有的子元素设置 refresh - if (refresh && child.isGroup()) { + if (child.isGroup()) { setChildrenRefresh(child.get('children'), region); } } @@ -113,7 +126,7 @@ function setChildrenRefresh(children: IElement[], region: Region) { function checkElementRefresh(shape: IElement, region: Region): boolean { const bbox = shape.cfg.cacheCanvasBBox; - const isAllow = bbox && intersectRect(bbox, region); + const isAllow = shape.cfg.isInView && bbox && intersectRect(bbox, region); return isAllow; } @@ -223,6 +236,9 @@ export function refreshElement(element, changeType) { } // 防止反复刷新 if (!element.get('hasChanged')) { + // 但是始终要标记为 hasChanged,便于后面进行局部渲染 + element.set('hasChanged', true); + // 本来只有局部渲染模式下,才需要记录更新的元素队列 // if (canvas.get('localRefresh')) { // canvas.refreshElement(element, changeType, canvas); @@ -235,8 +251,6 @@ export function refreshElement(element, changeType) { canvas.draw(); } } - // 但是始终要标记为 hasChanged,便于后面进行局部渲染 - element.set('hasChanged', true); } } } diff --git a/packages/g-canvas/src/util/hit.ts b/packages/g-canvas/src/util/hit.ts index 38eb3fc07..39282d085 100644 --- a/packages/g-canvas/src/util/hit.ts +++ b/packages/g-canvas/src/util/hit.ts @@ -27,9 +27,10 @@ function preTest(element: IElement, x: number, y: number) { } // 不允许被拾取,则返回 null // @ts-ignore - if (!isAllowCapture(element) && element.cfg.isInView === false) { + if (!isAllowCapture(element) || element.cfg.isInView === false) { return false; } + if (element.cfg.clipShape) { // 如果存在 clip const [refX, refY] = getRefXY(element, x, y); @@ -38,10 +39,11 @@ function preTest(element: IElement, x: number, y: number) { } } // @ts-ignore ,这个地方调用过于频繁 - let bbox = element.cfg.cacheCanvasBBox; - if (!bbox) { - bbox = element.getCanvasBBox(); - } + const bbox = element.cfg.cacheCanvasBBox || element.getCanvasBBox(); + // 如果没有缓存 bbox,则说明不可见 + // if (!bbox) { + // return false; + // } if (!(x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY)) { return false; } diff --git a/packages/g-canvas/tests/unit/canvas-draw-spec.js b/packages/g-canvas/tests/unit/canvas-draw-spec.js index 5b44702dc..667654d26 100644 --- a/packages/g-canvas/tests/unit/canvas-draw-spec.js +++ b/packages/g-canvas/tests/unit/canvas-draw-spec.js @@ -3,7 +3,7 @@ import Canvas from '../../src/canvas'; import Group from '../../src/group'; import { getColor } from '../get-color'; -const DELAY = 30; // 本来应该 16ms,但是测试时需要适当调大这个值 +const DELAY = 40; // 本来应该 16ms,但是测试时需要适当调大这个值 const dom = document.createElement('div'); document.body.appendChild(dom); @@ -271,9 +271,10 @@ describe('test canvas draw', () => { it('add empty group', () => { // 添加分组,不会导致重绘,但是需要考虑 add 已经存在子元素的 group的场景 // 由于目前还没有这样使用,所以可以不考虑这种场景 + // 为了简化,也计入 changed 范畴,便于后面的优化 group1 = canvas.addGroup(); - expect(group1.get('hasChanged')).eql(undefined); - expect(canvas.get('refreshElements').length).eql(0); + expect(group1.get('hasChanged')).eql(true); + expect(canvas.get('refreshElements').length).eql(1); }); it('add new group', (done) => { diff --git a/packages/g-canvas/tests/unit/canvas-refresh-spec.js b/packages/g-canvas/tests/unit/canvas-refresh-spec.js new file mode 100644 index 000000000..3f5b16ee4 --- /dev/null +++ b/packages/g-canvas/tests/unit/canvas-refresh-spec.js @@ -0,0 +1,169 @@ +const expect = require('chai').expect; +import Canvas from '../../src/canvas'; +import { getColor } from '../get-color'; + +const DELAY = 25; // 本来应该 16ms,但是测试时需要适当调大这个值 + +const dom = document.createElement('div'); +document.body.appendChild(dom); + +describe('prompt refresh test', () => { + const canvas = new Canvas({ + container: dom, + width: 300, + pixelRatio: 1, + height: 300, + }); + const ctx = canvas.get('context'); + let group; + let group1; + let group2; + let group11; + let group12; + + it('empty groups', (done) => { + group = canvas.addGroup(); + group1 = group.addGroup(); + group2 = group.addGroup(); + group11 = group1.addGroup({ zIndex: 10 }); + group12 = group2.addGroup({ zIndex: -1 }); + expect(canvas.get('refreshElements').length).eql(1); + setTimeout(() => { + expect(canvas.get('refreshElements').length).eql(0); + expect(group.get('hasChanged')).eql(false); + done(); + }, DELAY); + }); + + it('init add', (done) => { + group1.addShape({ + type: 'circle', + attrs: { + x: 100, + y: 100, + r: 10, + fill: '#0000ff', + }, + }); + expect(getColor(ctx, 100, 100)).eql('#000000'); + + group11.addShape({ + type: 'rect', + attrs: { + x: 200, + y: 100, + width: 20, + height: 20, + fill: '#ff0000', + }, + }); + expect(getColor(ctx, 201, 101)).eql('#000000'); + expect(canvas.get('refreshElements').length).eql(2); + setTimeout(() => { + expect(canvas.get('refreshElements').length).eql(0); + expect(getColor(ctx, 100, 100)).eql('#0000ff'); + expect(getColor(ctx, 201, 101)).eql('#ff0000'); + done(); + }, DELAY + 5); + }); + + it('shape move', (done) => { + const circle = group12.addShape({ + type: 'circle', + attrs: { + x: 50, + y: 50, + r: 5, + fill: '#00ff00', + }, + }); + group12.addGroup(); + setTimeout(() => { + expect(getColor(ctx, 50, 50)).eql('#00ff00'); + circle.translate(50, 50); + setTimeout(() => { + expect(getColor(ctx, 50, 50)).eql('#000000'); + expect(getColor(ctx, 100, 100)).eql('#00ff00'); + done(); + }, DELAY); + }, DELAY); + }); + + it('group move', (done) => { + group12.translate(20, 20); + expect(getColor(ctx, 100, 100)).eql('#00ff00'); + setTimeout(() => { + expect(getColor(ctx, 100, 100)).eql('#0000ff'); + expect(getColor(ctx, 120, 120)).eql('#00ff00'); + done(); + }, DELAY); + }); + + it('group move out', (done) => { + group12.translate(-200, 0); + setTimeout(() => { + expect(getColor(ctx, 120, 120)).eql('#000000'); + expect(group12.get('cacheCanvasBBox')).eql(null); + group12.translate(180, -20); + setTimeout(() => { + expect(getColor(ctx, 100, 100)).eql('#00ff00'); + expect(getColor(ctx, 95, 96)).eql('#0000ff'); + done(); + }, DELAY); + }, DELAY); + }); + + it('group sort', (done) => { + group1.sort(); + setTimeout(() => { + expect(getColor(ctx, 100, 100)).eql('#0000ff'); + group12.toFront(); + setTimeout(() => { + expect(getColor(ctx, 100, 100)).eql('#00ff00'); + done(); + }, DELAY); + }, DELAY); + }); + + it('group clip', (done) => { + group12.setClip({ + type: 'rect', + attrs: { + x: 100, + y: 100, + width: 10, + height: 10, + }, + }); + setTimeout(() => { + expect(getColor(ctx, 99, 99)).eql('#0000ff'); + expect(getColor(ctx, 101, 101)).eql('#00ff00'); + group12.getClip().attr({ x: 92, y: 92 }); + setTimeout(() => { + expect(getColor(ctx, 99, 99)).eql('#00ff00'); + done(); + }, DELAY); + }, DELAY); + }); + + it('canvas change size', (done) => { + group2.addShape({ + type: 'rect', + attrs: { + x: 310, + y: 310, + width: 10, + height: 10, + fill: '#fff000', + }, + }); + setTimeout(() => { + expect(getColor(ctx, 310, 310)).eql('#000000'); + canvas.changeSize(500, 500); + setTimeout(() => { + expect(getColor(ctx, 310, 310)).eql('#fff000'); + done(); + }, DELAY); + }, DELAY); + }); +}); diff --git a/packages/g-canvas/tests/unit/util/quick-hit-spec.js b/packages/g-canvas/tests/unit/util/quick-hit-spec.js index 727fa7959..cd37b65de 100644 --- a/packages/g-canvas/tests/unit/util/quick-hit-spec.js +++ b/packages/g-canvas/tests/unit/util/quick-hit-spec.js @@ -27,12 +27,9 @@ describe('quick hit test', () => { width: maxX, height: maxY, }); - - const root = canvas.addGroup(); - xit('no group and all in view', () => { for (let i = 0; i < count; i++) { - root.addShape('circle', { + canvas.addShape('circle', { attrs: { x: Math.random() * maxX, y: Math.random() * maxY, @@ -57,9 +54,9 @@ describe('quick hit test', () => { }); xit('no group some out view', (done) => { - root.clear(); + canvas.clear(); for (let i = 0; i < count; i++) { - root.addShape('circle', { + canvas.addShape('circle', { attrs: { x: Math.random() * maxX * 2, y: Math.random() * maxY * 2, @@ -86,14 +83,14 @@ describe('quick hit test', () => { }); xit('with group and matrix', (done) => { - root.clear(); + canvas.clear(); for (let i = 0; i < 10; i++) { - const group = root.addGroup(); + const group = canvas.addGroup(); group.translate(100 * Math.random(), 100 * Math.random()); } for (let i = 0; i < count; i++) { const index = i % 10; - const group = root.getChildByIndex(index); + const group = canvas.getChildByIndex(index); group.addShape('circle', { attrs: { x: Math.random() * maxX * 2, @@ -128,13 +125,14 @@ describe('quick hit test', () => { ['A', r, r, 0, 0, 0, x, y - r], ]; } + // xit('more groups', (done) => { - root.clear(); + const root = canvas.addGroup(); for (let i = 0; i < count * 2; i++) { const group = root.addGroup(); group.translate(100 * Math.random(), 100 * Math.random()); - const x = Math.random() * maxX * 4 - 100; - const y = Math.random() * maxY * 4 - 100; + const x = Math.random() * maxX - 100; + const y = Math.random() * maxY - 100; const r = Math.random() * 5; group.addShape('path', { name: 'node', From c43361c05494d4422592e83866170ad05903022e Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 17:51:31 +0800 Subject: [PATCH 09/11] chore(version): update version --- packages/g-base/package.json | 2 +- packages/g-svg/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/g-base/package.json b/packages/g-base/package.json index 1283f80b1..f63829fdd 100644 --- a/packages/g-base/package.json +++ b/packages/g-base/package.json @@ -54,7 +54,7 @@ "dependencies": { "@antv/event-emitter": "^0.1.1", "@antv/g-math": "^0.1.3", - "@antv/matrix-util": "^3.0.1-beta.1", + "@antv/matrix-util": "^3.0.2", "@antv/path-util": "~2.0.5", "@antv/util": "~2.0.0", "@types/d3-timer": "^1.0.9", diff --git a/packages/g-svg/package.json b/packages/g-svg/package.json index 0037ef00d..8b3009443 100644 --- a/packages/g-svg/package.json +++ b/packages/g-svg/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g-svg", - "version": "0.4.4", + "version": "0.5.0", "description": "A canvas library which providing 2d", "main": "lib/index.js", "module": "esm/index.js", @@ -54,7 +54,7 @@ }, "homepage": "https://github.com/antvis/g#readme", "dependencies": { - "@antv/g-base": "^0.4.4", + "@antv/g-base": "^0.5.0", "@antv/g-math": "^0.1.3", "@antv/util": "~2.0.0", "detect-browser": "^4.6.0" From 063a0103572463d8c199b24f005e1b90b97addff Mon Sep 17 00:00:00 2001 From: dxq613 Date: Fri, 24 Apr 2020 18:20:58 +0800 Subject: [PATCH 10/11] docs(comment): add comment --- packages/g-canvas/src/canvas.ts | 1 + packages/g-canvas/src/group.ts | 11 ++++++++++- packages/g-canvas/src/shape/base.ts | 6 ++++++ packages/g-canvas/src/util/draw.ts | 1 + packages/g-canvas/src/util/hit.ts | 5 +++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/g-canvas/src/canvas.ts b/packages/g-canvas/src/canvas.ts index 2e8947848..62e36c1ae 100644 --- a/packages/g-canvas/src/canvas.ts +++ b/packages/g-canvas/src/canvas.ts @@ -20,6 +20,7 @@ class Canvas extends AbstractCanvas { cfg['refreshElements'] = []; // 是否在视图内自动裁剪 cfg['clipView'] = true; + // 是否使用快速拾取的方案,默认为 false,上层可以打开 cfg['quickHit'] = false; return cfg; } diff --git a/packages/g-canvas/src/group.ts b/packages/g-canvas/src/group.ts index 31b421694..0ad73183f 100644 --- a/packages/g-canvas/src/group.ts +++ b/packages/g-canvas/src/group.ts @@ -40,12 +40,16 @@ class Group extends AbstractGroup { } } - cacheCanvasBBox() { + // 这个方法以前直接使用的 getCanvasBBox,由于 group 上没有缓存,所以每次重新计算,导致性能开销比较大 + // 大概能够节省全局渲染 15-20% 的性能,如果不在这里加缓存优化后 10W 个节点无法达到 5-6 ms,大概能够 30-40ms + private cacheCanvasBBox() { const children = this.cfg.children; const xArr = []; const yArr = []; each(children, (child) => { const bbox = child.cfg.cacheCanvasBBox; + // isInview 的判定是一旦图形或者分组渲染就要计算是否在视图内, + // 这个判定 10W 个图形下差不多能够节省 5-6 ms 的开销 if (bbox && child.cfg.isInView) { xArr.push(bbox.minX, bbox.maxX); yArr.push(bbox.minY, bbox.maxY); @@ -70,6 +74,8 @@ class Group extends AbstractGroup { const canvas = this.cfg.canvas; if (canvas) { const viewRange = canvas.getViewRange(); + // 如果这个地方判定 isInView == false 设置 bbox 为 false 的话,拾取的性能会更高 + // 但是目前 10W 图形的拾取在 2-5ms 内,这个优化意义不大,可以后期观察再看 this.set('isInView', intersectRect(bbox, viewRange)); } } else { @@ -82,6 +88,9 @@ class Group extends AbstractGroup { draw(context: CanvasRenderingContext2D, region?: Region) { const children = this.cfg.children as IElement[]; const allowDraw = region ? this.cfg.refresh : true; // 局部刷新需要判定 + // 这个地方需要判定,在 G6 的场景每个 group 都有 transform 的场景下性能会开销非常大 + // 通过 refresh 的判定,可以不刷新没有发生过变化的分组,不在视窗内的分组等等 + // 如果想进一步提升局部渲染性能,可以进一步优化 refresh 的判定,依然有潜力 if (children.length && allowDraw) { context.save(); // group 上的矩阵和属性也会应用到上下文上 diff --git a/packages/g-canvas/src/shape/base.ts b/packages/g-canvas/src/shape/base.ts index 9f44708b3..caa75f885 100644 --- a/packages/g-canvas/src/shape/base.ts +++ b/packages/g-canvas/src/shape/base.ts @@ -96,6 +96,9 @@ class ShapeBase extends AbstractShape { const bbox = this.getCanvasBBox(); if (!intersectRect(region, bbox)) { this.set('hasChanged', false); + // 存在多种情形需要更新 cacheCanvasBBox 和 isInview 的判定 + // 1. 之前图形在视窗内,但是现在不再视窗内 + // 2. 如果当前的图形以及父元素都没有发生过变化,refresh = false 不会走到这里,所以这里的图形都是父元素发生变化,但是没有在视图内的元素 if (this.cfg.isInView) { this._afterDraw(); } @@ -127,6 +130,9 @@ class ShapeBase extends AbstractShape { const bbox = this.getCanvasBBox(); const isInView = intersectRect(bbox, canvasBBox); this.set('isInView', isInView); + // 不再视窗内 cacheCanvasBBox 设置成 null,会提升局部渲染的性能, + // 因为在局部渲染影响的包围盒计算时不考虑这个图形的包围盒 + // 父元素 cacheCanvasBBox 计算的时候也不计算 if (isInView) { this.set('cacheCanvasBBox', bbox); } else { diff --git a/packages/g-canvas/src/util/draw.ts b/packages/g-canvas/src/util/draw.ts index 5f5c72ebb..9cf6d75ae 100644 --- a/packages/g-canvas/src/util/draw.ts +++ b/packages/g-canvas/src/util/draw.ts @@ -49,6 +49,7 @@ export function drawChildren(context: CanvasRenderingContext2D, children: IEleme } } +// 这个地方的逻辑比较复杂,简单画了一张图:https://www.yuque.com/antv/ou292n/pcgt5g#OW1QE export function checkRefresh(canvas, children: IElement[], region: Region) { const refreshElements = canvas.get('refreshElements'); // 先遍历需要刷新的元素,将这些元素的父元素也设置 refresh diff --git a/packages/g-canvas/src/util/hit.ts b/packages/g-canvas/src/util/hit.ts index 39282d085..a2471651a 100644 --- a/packages/g-canvas/src/util/hit.ts +++ b/packages/g-canvas/src/util/hit.ts @@ -41,6 +41,9 @@ function preTest(element: IElement, x: number, y: number) { // @ts-ignore ,这个地方调用过于频繁 const bbox = element.cfg.cacheCanvasBBox || element.getCanvasBBox(); // 如果没有缓存 bbox,则说明不可见 + // 注释掉的这段可能会加速拾取,上面的语句改写成 const bbox = element.cfg.cacheCanvasBBox; + // 这时候的拾取假设图形/分组在上一次绘制都在视窗内,但是上面已经判定了 isInView 所以意义不大 + // 现在还调用 element.getCanvasBBox(); 一个很大的原因是便于单元测试 // if (!bbox) { // return false; // } @@ -49,6 +52,8 @@ function preTest(element: IElement, x: number, y: number) { } return true; } + +// 这个方法复写了 g-base 的 getShape export function getShape(container: IContainer, x: number, y: number) { // 没有通过检测,则返回 null if (!preTest(container, x, y)) { From 34560643c986283816002b0754c96a87bfe1b9c0 Mon Sep 17 00:00:00 2001 From: dxq613 Date: Sun, 26 Apr 2020 11:08:46 +0800 Subject: [PATCH 11/11] chore(json): revert package.json version --- packages/g-base/package.json | 2 +- packages/g-canvas/package.json | 4 ++-- packages/g-svg/package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/g-base/package.json b/packages/g-base/package.json index f63829fdd..04a487f99 100644 --- a/packages/g-base/package.json +++ b/packages/g-base/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g-base", - "version": "0.5.0", + "version": "0.4.4", "description": "A common util collection for antv projects", "main": "lib/index.js", "module": "esm/index.js", diff --git a/packages/g-canvas/package.json b/packages/g-canvas/package.json index 720af9809..3469757f0 100644 --- a/packages/g-canvas/package.json +++ b/packages/g-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g-canvas", - "version": "0.5.0", + "version": "0.4.9", "description": "A canvas library which providing 2d", "main": "lib/index.js", "module": "esm/index.js", @@ -55,7 +55,7 @@ }, "homepage": "https://github.com/antvis/g#readme", "dependencies": { - "@antv/g-base": "^0.5.0", + "@antv/g-base": "^0.4.4", "@antv/g-math": "^0.1.3", "@antv/path-util": "~2.0.5", "@antv/util": "~2.0.0", diff --git a/packages/g-svg/package.json b/packages/g-svg/package.json index 8b3009443..0037ef00d 100644 --- a/packages/g-svg/package.json +++ b/packages/g-svg/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g-svg", - "version": "0.5.0", + "version": "0.4.4", "description": "A canvas library which providing 2d", "main": "lib/index.js", "module": "esm/index.js", @@ -54,7 +54,7 @@ }, "homepage": "https://github.com/antvis/g#readme", "dependencies": { - "@antv/g-base": "^0.5.0", + "@antv/g-base": "^0.4.4", "@antv/g-math": "^0.1.3", "@antv/util": "~2.0.0", "detect-browser": "^4.6.0"