diff --git a/packages/g-base/package.json b/packages/g-base/package.json index 25709c020..d17f4003a 100644 --- a/packages/g-base/package.json +++ b/packages/g-base/package.json @@ -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" }, 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); } diff --git a/packages/g-base/src/abstract/element.ts b/packages/g-base/src/abstract/element.ts index 480348dee..a94e3910a 100644 --- a/packages/g-base/src/abstract/element.ts +++ b/packages/g-base/src/abstract/element.ts @@ -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() { @@ -294,10 +301,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 { @@ -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 }); diff --git a/packages/g-base/src/abstract/shape.ts b/packages/g-base/src/abstract/shape.ts index acb73dfcc..d688f1ccc 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,12 +33,12 @@ abstract class AbstractShape extends Element implements IShape { } // 计算相对于画布的包围盒 getCanvasBBox(): BBox { - let canvasBox = this.get('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, diff --git a/packages/g-canvas/package.json b/packages/g-canvas/package.json index ad9eddd35..ea4dae9ad 100644 --- a/packages/g-canvas/package.json +++ b/packages/g-canvas/package.json @@ -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" @@ -59,6 +59,7 @@ "@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 ed618dcd0..00aa05460 100644 --- a/packages/g-canvas/src/canvas.ts +++ b/packages/g-canvas/src/canvas.ts @@ -4,7 +4,7 @@ import { IElement } from './interfaces'; import { getShape } from './util/hit'; import * as Shape from './shape'; import Group from './group'; -import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView } from './util/draw'; +import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView, checkRefresh, clearChanged } from './util/draw'; import { getPixelRatio, requestAnimationFrame, clearAnimationFrame } from './util/util'; class Canvas extends AbstractCanvas { @@ -19,6 +19,7 @@ class Canvas extends AbstractCanvas { cfg['refreshElements'] = []; // 是否在视图内自动裁剪 cfg['clipView'] = true; + // 是否使用快速拾取的方案,默认为 false,上层可以打开 cfg['quickHit'] = false; return cfg; } @@ -60,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, }; } @@ -95,10 +96,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() { @@ -171,6 +175,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) { @@ -182,9 +187,18 @@ 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(); + } 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 57257ed92..0ad73183f 100644 --- a/packages/g-canvas/src/group.ts +++ b/packages/g-canvas/src/group.ts @@ -4,8 +4,9 @@ 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'; +import { intersectRect } from './util/util'; class Group extends AbstractGroup { /** @@ -39,9 +40,58 @@ class Group extends AbstractGroup { } } + // 这个方法以前直接使用的 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); + } + }); + 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(); + // 如果这个地方判定 isInView == false 设置 bbox 为 false 的话,拾取的性能会更高 + // 但是目前 10W 图形的拾取在 2-5ms 内,这个优化意义不大,可以后期观察再看 + 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[]; - if (children.length) { + 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 上的矩阵和属性也会应用到上下文上 // 先将 attrs 应用到上下文中,再设置 clip。因为 clip 应该被当前元素的 matrix 所影响 @@ -49,9 +99,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.set('cacheCanvasBBox', this.getCanvasBBox()); + 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 0bb260c76..caa75f885 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'; @@ -84,36 +84,68 @@ class ShapeBase extends AbstractShape { // 绘制图形时需要考虑 region 限制 draw(context: CanvasRenderingContext2D, region?: Region) { - const clip = this.getClip(); - // 如果指定了区域,当与指定区域相交时,才会触发渲染 + const clip = this.cfg.clipShape; + // 如果指定了 region,同时不允许刷新时,直接返回 if (region) { + if (this.cfg.refresh === false) { + // this._afterDraw(); + 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); + // 存在多种情形需要更新 cacheCanvasBBox 和 isInview 的判定 + // 1. 之前图形在视窗内,但是现在不再视窗内 + // 2. 如果当前的图形以及父元素都没有发生过变化,refresh = false 不会走到这里,所以这里的图形都是父元素发生变化,但是没有在视图内的元素 + if (this.cfg.isInView) { + this._afterDraw(); + } return; } } 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() { - 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); + // 不再视窗内 cacheCanvasBBox 设置成 null,会提升局部渲染的性能, + // 因为在局部渲染影响的包围盒计算时不考虑这个图形的包围盒 + // 父元素 cacheCanvasBBox 计算的时候也不计算 + if (isInView) { + this.set('cacheCanvasBBox', bbox); + } else { + this.set('cacheCanvasBBox', null); + } } + } + + _afterDraw() { + 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 8ef231814..9cf6d75ae 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'; @@ -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(); @@ -49,6 +49,88 @@ 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 + 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.cfg.children, region); + } + } else if (child.cfg.hasChanged) { + // 如果节点发生了 change,则需要级联设置子元素的 refresh + child.cfg.refresh = true; + if (child.isGroup()) { + setChildrenRefresh(child.cfg.children, region); + } + } else { + // 这个分支说明此次局部刷新,所有的节点和父元素没有发生变化,仅需要检查包围盒(缓存)是否相交即可 + child.cfg.refresh = checkElementRefresh(child, 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; + // 获取缓存的 bbox,如果这个 bbox 还存在则说明父元素不是矩阵发生了改变 + // const bbox = child.cfg.canvasBBox; + // if (bbox) { + // // 如果这时候 + // refresh = intersectRect(bbox, region); + // } + child.cfg.refresh = true; + // 如果需要刷新当前节点,所有的子元素设置 refresh + if (child.isGroup()) { + setChildrenRefresh(child.get('children'), region); + } + } +} + +function checkElementRefresh(shape: IElement, region: Region): boolean { + const bbox = shape.cfg.cacheCanvasBBox; + const isAllow = shape.cfg.isInView && bbox && intersectRect(bbox, region); + return isAllow; +} + // 绘制 path export function drawPath(shape, context, attrs, arcParamsCache) { const { path, startArrow, endArrow } = attrs; @@ -155,16 +237,21 @@ export function refreshElement(element, changeType) { } // 防止反复刷新 if (!element.get('hasChanged')) { + // 但是始终要标记为 hasChanged,便于后面进行局部渲染 + element.set('hasChanged', true); + // 本来只有局部渲染模式下,才需要记录更新的元素队列 // if (canvas.get('localRefresh')) { // 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(); + } } - element.set('hasChanged', true); } } } diff --git a/packages/g-canvas/src/util/hit.ts b/packages/g-canvas/src/util/hit.ts index 38eb3fc07..a2471651a 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,15 +39,21 @@ 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,则说明不可见 + // 注释掉的这段可能会加速拾取,上面的语句改写成 const bbox = element.cfg.cacheCanvasBBox; + // 这时候的拾取假设图形/分组在上一次绘制都在视窗内,但是上面已经判定了 isInView 所以意义不大 + // 现在还调用 element.getCanvasBBox(); 一个很大的原因是便于单元测试 + // if (!bbox) { + // return false; + // } if (!(x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY)) { return false; } return true; } + +// 这个方法复写了 g-base 的 getShape export function getShape(container: IContainer, x: number, y: number) { // 没有通过检测,则返回 null if (!preTest(container, x, y)) { 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; diff --git a/packages/g-canvas/tests/unit/canvas-draw-spec.js b/packages/g-canvas/tests/unit/canvas-draw-spec.js index e89660334..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 = 25; // 本来应该 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) => { @@ -301,11 +302,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 +317,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/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 82abf6e78..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,7 +27,6 @@ describe('quick hit test', () => { width: maxX, height: maxY, }); - xit('no group and all in view', () => { for (let i = 0; i < count; i++) { canvas.addShape('circle', { @@ -126,13 +125,14 @@ describe('quick hit test', () => { ['A', r, r, 0, 0, 0, x, y - r], ]; } + // xit('more groups', (done) => { - canvas.clear(); + const root = canvas.addGroup(); 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; + const x = Math.random() * maxX - 100; + const y = Math.random() * maxY - 100; const r = Math.random() * 5; group.addShape('path', { name: 'node',