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 };