diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7f3ad1fc8..1e45ca9e5 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -107,6 +107,7 @@ module.exports = { { functions: false, classes: false }, ], '@typescript-eslint/no-redeclare': ['error'], + '@typescript-eslint/no-this-alias': ['error', { allowedNames: ['self'] }], '@typescript-eslint/restrict-template-expressions': 'warn', '@typescript-eslint/return-await': 'warn', '@typescript-eslint/default-param-last': 'warn', diff --git a/__tests__/demos/bugfix/1760.ts b/__tests__/demos/bugfix/1760.ts index ea0ba6512..92eaf2929 100644 --- a/__tests__/demos/bugfix/1760.ts +++ b/__tests__/demos/bugfix/1760.ts @@ -3,7 +3,7 @@ import { Canvas, Path, Line } from '@antv/g'; /** * @see https://github.com/antvis/G/issues/1760 * @see https://github.com/antvis/G/issues/1790 - * @see https://github.com/antvis/G/pull/1808 + * @see https://github.com/antvis/G/pull/1809 */ export async function issue_1760(context: { canvas: Canvas }) { const { canvas } = context; diff --git a/__tests__/demos/perf/attr-update.ts b/__tests__/demos/perf/attr-update.ts new file mode 100644 index 000000000..fe9ba15cf --- /dev/null +++ b/__tests__/demos/perf/attr-update.ts @@ -0,0 +1,88 @@ +import { Rect, Group, CanvasEvent } from '@antv/g'; +import type { Canvas } from '@antv/g'; + +export async function attrUpdate(context: { canvas: Canvas }) { + const { canvas } = context; + console.log(canvas); + + await canvas.ready; + + const { width, height } = canvas.getConfig(); + const count = 2e4; + const root = new Group(); + const rects = []; + + const perfStore: { [k: string]: { count: number; time: number } } = { + update: { count: 0, time: 0 }, + setAttribute: { count: 0, time: 0 }, + }; + + function updatePerf(key: string, time: number) { + perfStore[key].count++; + perfStore[key].time += time; + console.log( + `average ${key} time: `, + perfStore[key].time / perfStore[key].count, + ); + } + + function update() { + // const startTime = performance.now(); + // console.time('update'); + + const rectsToRemove = []; + + // const startTime0 = performance.now(); + // console.time('setAttribute'); + for (let i = 0; i < count; i++) { + const rect = rects[i]; + rect.x -= rect.speed; + (rect.el as Rect).setAttribute('x', rect.x); + if (rect.x + rect.size < 0) rectsToRemove.push(i); + } + // console.timeEnd('setAttribute'); + // updatePerf('setAttribute', performance.now() - startTime0); + + rectsToRemove.forEach((i) => { + rects[i].x = width + rects[i].size / 2; + }); + + // console.timeEnd('update'); + // updatePerf('update', performance.now() - startTime); + } + + function render() { + for (let i = 0; i < count; i++) { + const x = Math.random() * width; + const y = Math.random() * height; + const size = 10 + Math.random() * 40; + const speed = 1 + Math.random(); + + const rect = new Rect({ + style: { + x, + y, + width: size, + height: size, + fill: 'white', + stroke: 'black', + }, + }); + root.appendChild(rect); + rects[i] = { x, y, size, speed, el: rect }; + } + } + + render(); + canvas.addEventListener(CanvasEvent.BEFORE_RENDER, () => update()); + + canvas.appendChild(root); + + canvas.addEventListener( + 'rerender', + () => { + // console.timeEnd('render'); + }, + { once: true }, + ); +} diff --git a/__tests__/demos/perf/index.ts b/__tests__/demos/perf/index.ts index 7534392e1..efcf468e5 100644 --- a/__tests__/demos/perf/index.ts +++ b/__tests__/demos/perf/index.ts @@ -1,3 +1,4 @@ export { circles } from './circles'; export { rects } from './rect'; export { image } from './image'; +export { attrUpdate } from './attr-update'; diff --git a/__tests__/index.html b/__tests__/index.html index 9049dd773..77ca2c864 100644 --- a/__tests__/index.html +++ b/__tests__/index.html @@ -1,4 +1,4 @@ - + @@ -51,6 +51,8 @@ + +
diff --git a/__tests__/main.ts b/__tests__/main.ts index 94ca49aaa..36b560618 100644 --- a/__tests__/main.ts +++ b/__tests__/main.ts @@ -45,7 +45,7 @@ const renderers = { }; const app = document.getElementById('app') as HTMLElement; let currentContainer = document.createElement('div'); -let canvas; +let canvas: Canvas; let prevAfter; const normalizeName = (name: string) => name.replace(/-/g, '').toLowerCase(); const renderOptions = (keyword = '') => { @@ -58,7 +58,7 @@ const renderOptions = (keyword = '') => { // Select for chart. const selectChart = document.createElement('select') as HTMLSelectElement; -selectChart.style.margin = '1em'; +selectChart.style.margin = '1em 1em 1em 120px'; renderOptions(); selectChart.onchange = () => { const { value } = selectChart; @@ -229,7 +229,18 @@ function createSpecRender(object) { const gui = new lil.GUI({ autoPlace: false }); $div.appendChild(gui.domElement); - await generate({ canvas, renderer, container: $div, gui }); + // @see https://github.com/Darsain/fpsmeter/wiki/Options + const fpsMeter = new window.FPSMeter({ + theme: 'light', + heat: 1, + graph: 1, + }); + + await generate({ canvas, renderer, container: $div, gui, fpsMeter }); + + canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + fpsMeter.tick(); + }); if ( selectRenderer.value === 'canvas' && diff --git a/__tests__/tsconfig.json b/__tests__/tsconfig.json deleted file mode 100644 index 1416a64c7..000000000 --- a/__tests__/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "types": ["jest"], - "moduleResolution": "node", - "lib": ["ESNext", "DOM"] - }, - "extends": "../tsconfig.json" -} diff --git a/packages/g-lite/src/Canvas.ts b/packages/g-lite/src/Canvas.ts index 05b880aeb..0d9544789 100644 --- a/packages/g-lite/src/Canvas.ts +++ b/packages/g-lite/src/Canvas.ts @@ -398,9 +398,7 @@ export class Canvas extends EventTarget implements ICanvas { this.dispatchEvent(new CustomEvent(CanvasEvent.BEFORE_DESTROY)); } if (this.frameId) { - const cancelRAF = - this.getConfig().cancelAnimationFrame || cancelAnimationFrame; - cancelRAF(this.frameId); + this.cancelAnimationFrame(this.frameId); } // unmount all children @@ -440,8 +438,6 @@ export class Canvas extends EventTarget implements ICanvas { clearEventRetain(beforeRenderEvent); clearEventRetain(rerenderEvent); clearEventRetain(afterRenderEvent); - - this.cancelAnimationFrame(this.frameId); } /** diff --git a/packages/g-lite/src/dom/Node.ts b/packages/g-lite/src/dom/Node.ts index e428dc302..fa552b1d6 100644 --- a/packages/g-lite/src/dom/Node.ts +++ b/packages/g-lite/src/dom/Node.ts @@ -361,16 +361,19 @@ export abstract class Node extends EventTarget implements INode { /** * iterate current node and its descendants * @param callback - callback to execute for each node, return false to break - * @param assigned - whether to iterate assigned nodes */ forEach(callback: (o: INode) => void | boolean) { - const result = callback(this); + const stack: INode[] = [this]; - if (result !== false) { - const nodes = this.childNodes; - const length = nodes.length; - for (let i = 0; i < length; i++) { - nodes[i].forEach(callback); + while (stack.length > 0) { + const node = stack.pop(); + const result = callback(node); + if (result === false) { + break; + } + + for (let i = node.childNodes.length - 1; i >= 0; i--) { + stack.push(node.childNodes[i]); } } } diff --git a/packages/g-lite/src/services/RenderingService.ts b/packages/g-lite/src/services/RenderingService.ts index 40eeaccab..f82062f75 100644 --- a/packages/g-lite/src/services/RenderingService.ts +++ b/packages/g-lite/src/services/RenderingService.ts @@ -220,52 +220,62 @@ export class RenderingService { canvasConfig: Partial, renderingContext: RenderingContext, ) { + const self = this; const { enableDirtyCheck, enableCulling } = canvasConfig.renderer.getConfig(); - // TODO: relayout - - // dirtycheck first - const { renderable } = displayObject; - // eslint-disable-next-line no-nested-ternary - const objectChanged = enableDirtyCheck - ? // @ts-ignore - renderable.dirty || renderingContext.dirtyRectangleRenderingDisabled - ? displayObject - : null - : displayObject; - - if (objectChanged) { - const objectToRender = enableCulling - ? this.hooks.cull.call(objectChanged, this.context.camera) - : objectChanged; - - if (objectToRender) { - this.stats.rendered++; - renderingContext.renderListCurrentFrame.push(objectToRender); + function internalRenderSingleDisplayObject(object: DisplayObject) { + // TODO: relayout + + // dirtycheck first + const { renderable, sortable } = object; + // eslint-disable-next-line no-nested-ternary + const objectChanged = enableDirtyCheck + ? // @ts-ignore + renderable.dirty || renderingContext.dirtyRectangleRenderingDisabled + ? object + : null + : object; + + if (objectChanged) { + const objectToRender = enableCulling + ? self.hooks.cull.call(objectChanged, self.context.camera) + : objectChanged; + + if (objectToRender) { + self.stats.rendered += 1; + renderingContext.renderListCurrentFrame.push(objectToRender); + } } - } - displayObject.renderable.dirty = false; - displayObject.sortable.renderOrder = this.zIndexCounter++; + renderable.dirty = false; + sortable.renderOrder = self.zIndexCounter; - this.stats.total++; + self.zIndexCounter += 1; + self.stats.total += 1; - // sort is very expensive, use cached result if possible - const { sortable } = displayObject; - if (sortable.dirty) { - this.sort(displayObject, sortable); - sortable.dirty = false; - sortable.dirtyChildren = []; - sortable.dirtyReason = undefined; + // sort is very expensive, use cached result if possible + if (sortable.dirty) { + self.sort(object, sortable); + sortable.dirty = false; + sortable.dirtyChildren = []; + sortable.dirtyReason = undefined; + } } - // recursive rendering its children - (sortable.sorted || displayObject.childNodes).forEach( - (child: DisplayObject) => { - this.renderDisplayObject(child, canvasConfig, renderingContext); - }, - ); + const stack = [displayObject]; + + while (stack.length > 0) { + const currentObject = stack.pop(); + + internalRenderSingleDisplayObject(currentObject); + + // recursive rendering its children + const objects = currentObject.sortable.sorted || currentObject.childNodes; + for (let i = objects.length - 1; i >= 0; i--) { + stack.push(objects[i] as unknown as DisplayObject); + } + } } private sort(displayObject: DisplayObject, sortable: Sortable) { diff --git a/packages/g-plugin-canvas-path-generator/src/index.ts b/packages/g-plugin-canvas-path-generator/src/index.ts index a416bbd01..343316223 100644 --- a/packages/g-plugin-canvas-path-generator/src/index.ts +++ b/packages/g-plugin-canvas-path-generator/src/index.ts @@ -10,7 +10,9 @@ import { RectPath, } from './paths'; -export class Plugin extends AbstractRendererPlugin { +export class Plugin extends AbstractRendererPlugin<{ + pathGeneratorFactory: Record>; +}> { name = 'canvas-path-generator'; init(): void { const pathGeneratorFactory: Record> = { @@ -26,6 +28,7 @@ export class Plugin extends AbstractRendererPlugin { [Shape.IMAGE]: undefined, [Shape.HTML]: undefined, [Shape.MESH]: undefined, + [Shape.FRAGMENT]: undefined, }; // @ts-ignore diff --git a/packages/g-plugin-canvas-renderer/src/CanvasRendererPlugin.ts b/packages/g-plugin-canvas-renderer/src/CanvasRendererPlugin.ts index d6460cabb..60344e656 100644 --- a/packages/g-plugin-canvas-renderer/src/CanvasRendererPlugin.ts +++ b/packages/g-plugin-canvas-renderer/src/CanvasRendererPlugin.ts @@ -19,10 +19,10 @@ import { Shape, Node, } from '@antv/g-lite'; -import type { PathGenerator } from '@antv/g-plugin-canvas-path-generator'; import { isNil } from '@antv/util'; import { mat4, vec3 } from 'gl-matrix'; import type { CanvasRendererPluginOptions } from './interfaces'; +import type { Plugin } from '.'; interface Rect { x: number; @@ -39,9 +39,9 @@ interface Rect { export class CanvasRendererPlugin implements RenderingPlugin { static tag = 'CanvasRenderer'; - private context: RenderingPluginContext; + private context: Plugin['context']; - private pathGeneratorFactory: Record>; + private pathGeneratorFactory: Plugin['context']['pathGeneratorFactory']; /** * RBush used in dirty rectangle rendering @@ -76,7 +76,7 @@ export class CanvasRendererPlugin implements RenderingPlugin { private vec3d = vec3.create(); apply(context: RenderingPluginContext, runtime: GlobalRuntime) { - this.context = context; + this.context = context as unknown as Plugin['context']; const { config, @@ -86,9 +86,14 @@ export class CanvasRendererPlugin implements RenderingPlugin { rBushRoot, // @ts-ignore pathGeneratorFactory, - } = context; + } = this.context; + + config.renderer.getConfig().enableDirtyCheck = false; + config.renderer.getConfig().enableDirtyRectangleRendering = false; + this.rBush = rBushRoot; this.pathGeneratorFactory = pathGeneratorFactory; + const contextService = context.contextService as ContextService; @@ -164,9 +169,12 @@ export class CanvasRendererPlugin implements RenderingPlugin { ratio > dirtyObjectRatioThreshold); if (context) { - context.resetTransform - ? context.resetTransform() - : context.setTransform(1, 0, 0, 1, 0, 0); + if (typeof context.resetTransform === 'function') { + context.resetTransform(); + } else { + context.setTransform(1, 0, 0, 1, 0, 0); + } + if (this.clearFullScreen) { this.clearRect( context, @@ -180,26 +188,37 @@ export class CanvasRendererPlugin implements RenderingPlugin { } }); + /** + * render objects by z-index + * + * - The level of the child node will be affected by the level of the parent node + */ const renderByZIndex = ( object: DisplayObject, context: CanvasRenderingContext2D, ) => { - if (object.isVisible() && !object.isCulled()) { - this.renderDisplayObject( - object, - context, - this.context, - this.restoreStack, - runtime, - ); - } + const stack = [object]; - const sorted = object.sortable.sorted || object.childNodes; + while (stack.length > 0) { + const currentObject = stack.pop(); - // should account for z-index - sorted.forEach((child: DisplayObject) => { - renderByZIndex(child, context); - }); + if (currentObject.isVisible() && !currentObject.isCulled()) { + this.renderDisplayObject( + currentObject, + context, + this.context, + this.restoreStack, + runtime, + ); + } + + const objects = + currentObject.sortable.sorted || currentObject.childNodes; + // should account for z-index + for (let i = objects.length - 1; i >= 0; i--) { + stack.push(objects[i] as unknown as DisplayObject); + } + } }; // render at the end of frame @@ -219,8 +238,10 @@ export class CanvasRendererPlugin implements RenderingPlugin { mat4.multiply(this.vpMatrix, this.dprMatrix, camera.getOrthoMatrix()); if (this.clearFullScreen) { + // console.time('renderByZIndex'); // console.log('canvas renderer fcp...', renderingContext.root.childNodes); renderByZIndex(renderingContext.root, context); + // console.timeEnd('renderByZIndex'); } else { // console.log('canvas renderer next...', this.renderQueue); // merge removed AABB @@ -381,8 +402,6 @@ export class CanvasRendererPlugin implements RenderingPlugin { ) { const { nodeName } = object; - // console.log('canvas render:', object); - // restore to its ancestor const parent = restoreStack[restoreStack.length - 1]; diff --git a/packages/g-plugin-canvas-renderer/src/index.ts b/packages/g-plugin-canvas-renderer/src/index.ts index 19511fc97..9af7608c0 100644 --- a/packages/g-plugin-canvas-renderer/src/index.ts +++ b/packages/g-plugin-canvas-renderer/src/index.ts @@ -1,9 +1,12 @@ import { AbstractRendererPlugin, Shape } from '@antv/g-lite'; +import type { PathGenerator } from '@antv/g-plugin-canvas-path-generator'; import { CanvasRendererPlugin } from './CanvasRendererPlugin'; -import type { StyleRenderer } from './shapes/styles'; -import { DefaultRenderer } from './shapes/styles/Default'; -import { ImageRenderer } from './shapes/styles/Image'; -import { TextRenderer } from './shapes/styles/Text'; +import { + type StyleRenderer, + DefaultRenderer, + TextRenderer, + ImageRenderer, +} from './shapes/styles'; import type { CanvasRendererPluginOptions } from './interfaces'; export * from './shapes/styles'; @@ -11,6 +14,7 @@ export * from './shapes/styles'; export class Plugin extends AbstractRendererPlugin<{ defaultStyleRendererFactory: Record; styleRendererFactory: Record; + pathGeneratorFactory: Record>; }> { name = 'canvas-renderer'; @@ -43,6 +47,7 @@ export class Plugin extends AbstractRendererPlugin<{ [Shape.GROUP]: undefined, [Shape.HTML]: undefined, [Shape.MESH]: undefined, + [Shape.FRAGMENT]: undefined, }; this.context.defaultStyleRendererFactory = defaultStyleRendererFactory; diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Circle.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Circle.ts deleted file mode 100644 index 037910f8b..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Circle.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class CircleRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Ellipse.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Ellipse.ts deleted file mode 100644 index 94b0f6748..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Ellipse.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class EllipseRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Line.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Line.ts deleted file mode 100644 index 976c7c7ea..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Line.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class LineRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Path.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Path.ts deleted file mode 100644 index b74741add..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Path.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class PathRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Polygon.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Polygon.ts deleted file mode 100644 index 31d96b148..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Polygon.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class PolygonRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Polyline.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Polyline.ts deleted file mode 100644 index 901959c34..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Polyline.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class PolylineRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/Rect.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/Rect.ts deleted file mode 100644 index b11da2682..000000000 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/Rect.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { DefaultRenderer } from './Default'; - -export class RectRenderer extends DefaultRenderer {} diff --git a/packages/g-plugin-canvas-renderer/src/shapes/styles/index.ts b/packages/g-plugin-canvas-renderer/src/shapes/styles/index.ts index d40ab8bdb..079fe4be4 100644 --- a/packages/g-plugin-canvas-renderer/src/shapes/styles/index.ts +++ b/packages/g-plugin-canvas-renderer/src/shapes/styles/index.ts @@ -1,10 +1,13 @@ +import { DefaultRenderer } from './Default'; + export * from './interfaces'; -export * from './Image'; -export * from './Text'; -export * from './Rect'; -export * from './Circle'; -export * from './Ellipse'; -export * from './Line'; -export * from './Polyline'; -export * from './Polygon'; -export * from './Path'; +export { DefaultRenderer }; +export { ImageRenderer } from './Image'; +export { TextRenderer } from './Text'; +export { DefaultRenderer as RectRenderer }; +export { DefaultRenderer as CircleRenderer }; +export { DefaultRenderer as LineRenderer }; +export { DefaultRenderer as PolylineRenderer }; +export { DefaultRenderer as PolygonRenderer }; +export { DefaultRenderer as PathRenderer }; +export { DefaultRenderer as EllipseRenderer };