refactor: refactor canvas, support switch renderer (#6034)

* chore: update pnpm workspace

* refactor(runtime): refactor canvas, support setRenderer

* refactor(runtime): graph adpat canvas, remove get/set background

* refactor: adjust $layer property

* refactor: refactor fullscreen plugin

* refactor: base-element add context property

* refactor: canvas add getRenderer API
This commit is contained in:
Aaron 2024-07-15 19:58:58 +08:00 committed by GitHub
parent 5b993b007d
commit 0e911c2ad9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 221 additions and 302 deletions

View File

@ -1,4 +1,3 @@
import type { IRenderer } from '@antv/g';
import { resetEntityCounter } from '@antv/g'; import { resetEntityCounter } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Renderer as SVGRenderer } from '@antv/g-svg'; import { Renderer as SVGRenderer } from '@antv/g-svg';
@ -41,12 +40,11 @@ export function createGraphCanvas(
} as unknown as HTMLCanvasElement; } as unknown as HTMLCanvasElement;
const context = new OffscreenCanvasContext(offscreenNodeCanvas); const context = new OffscreenCanvasContext(offscreenNodeCanvas);
const instance = getRenderer(renderer) as any as IRenderer;
return { return {
container, container,
width, width,
height, height,
renderer: () => instance, renderer: () => getRenderer(renderer),
document: container.ownerDocument, document: container.ownerDocument,
offscreenCanvas: offscreenNodeCanvas, offscreenCanvas: offscreenNodeCanvas,
}; };

View File

@ -99,7 +99,11 @@ export async function toMatchSnapshot(
detail?: string, detail?: string,
options: ToMatchSVGSnapshotOptions = {}, options: ToMatchSVGSnapshotOptions = {},
) { ) {
return await toMatchSVGSnapshot(Object.values(graph.getCanvas().canvas), ...getSnapshotDir(dir, detail), options); return await toMatchSVGSnapshot(
Object.values(graph.getCanvas().getLayers()),
...getSnapshotDir(dir, detail),
options,
);
} }
export async function toMatchAnimation( export async function toMatchAnimation(
@ -126,7 +130,7 @@ export async function toMatchAnimation(
animation.currentTime = frame; animation.currentTime = frame;
await sleep(32); await sleep(32);
const result = await toMatchSVGSnapshot( const result = await toMatchSVGSnapshot(
Object.values(graph.getCanvas().canvas), Object.values(graph.getCanvas().getCanvases().canvas),
...getSnapshotDir(dir, `${detail}-${frame}`), ...getSnapshotDir(dir, `${detail}-${frame}`),
options, options,
); );

View File

@ -23,7 +23,7 @@ export abstract class BaseNode3D<S extends BaseNode3DStyleProps> extends BaseNod
public type = 'node-3d'; public type = 'node-3d';
protected get plugin() { protected get plugin() {
const renderer = this.attributes.context!.canvas.renderers['main']; const renderer = this.context.canvas.getRenderer('main');
const plugin = renderer.getPlugin('device-renderer'); const plugin = renderer.getPlugin('device-renderer');
return plugin as unknown as Plugin; return plugin as unknown as Plugin;
} }

View File

@ -78,7 +78,7 @@ export const caseIndentedTree: TestCase = async (context) => {
} }
protected get childrenData() { protected get childrenData() {
return this.attributes.context!.model.getChildrenData(this.id); return this.context!.model.getChildrenData(this.id);
} }
protected getKeyStyle(attributes: Required<IndentedNodeStyleProps>): RectStyleProps { protected getKeyStyle(attributes: Required<IndentedNodeStyleProps>): RectStyleProps {
@ -147,7 +147,7 @@ export const caseIndentedTree: TestCase = async (context) => {
this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => { this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => {
event.stopPropagation(); event.stopPropagation();
attributes.context!.graph.emit(TreeEvent.COLLAPSE_EXPAND, { this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
id: this.id, id: this.id,
collapsed: false, collapsed: false,
}); });
@ -184,7 +184,7 @@ export const caseIndentedTree: TestCase = async (context) => {
this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => { this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => {
event.stopPropagation(); event.stopPropagation();
attributes.context!.graph.emit(TreeEvent.COLLAPSE_EXPAND, { this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
id: this.id, id: this.id,
collapsed: !attributes.collapsed, collapsed: !attributes.collapsed,
}); });
@ -221,7 +221,7 @@ export const caseIndentedTree: TestCase = async (context) => {
this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => { this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => {
event.stopPropagation(); event.stopPropagation();
attributes.context!.graph.emit(TreeEvent.ADD_CHILD, { id: this.id }); this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id });
}); });
} }

View File

@ -64,7 +64,7 @@ async function render() {
// render // render
const { Renderer, Demo, Animation, Theme } = options; const { Renderer, Demo, Animation, Theme } = options;
const canvas = createGraphCanvas($container, 500, 500, Renderer); const canvas = createGraphCanvas($container, 500, 500, Renderer);
await canvas.init(); await canvas.ready;
const testCase = demos[Demo as keyof typeof demos]; const testCase = demos[Demo as keyof typeof demos];
if (!testCase) return; if (!testCase) return;

View File

@ -4,11 +4,7 @@ import { createGraphCanvas } from '@@/utils';
describe('Canvas', () => { describe('Canvas', () => {
const svg = createGraphCanvas(null, 500, 500, 'svg'); const svg = createGraphCanvas(null, 500, 500, 'svg');
beforeAll(async () => { beforeAll(async () => {
await svg.init(); await svg.ready;
});
it('getRendererType', () => {
expect(svg.getRendererType()).toBe('svg');
}); });
it('context', () => { it('context', () => {

View File

@ -43,10 +43,6 @@ describe('Graph', () => {
}); });
}); });
it('setBackground/getBackground', () => {
expect(graph.getBackground()).toEqual('#ffffff');
});
it('getSize', () => { it('getSize', () => {
expect(graph.getSize()).toEqual([500, 500]); expect(graph.getSize()).toEqual([500, 500]);
}); });

View File

@ -3,7 +3,6 @@ import { Graph } from '@/src';
import { Circle } from '@/src/elements'; import { Circle } from '@/src/elements';
import { Canvas } from '@/src/runtime/canvas'; import { Canvas } from '@/src/runtime/canvas';
import type { Node, Point } from '@/src/types'; import type { Node, Point } from '@/src/types';
import type { IRenderer } from '@antv/g';
import { resetEntityCounter } from '@antv/g'; import { resetEntityCounter } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Renderer as SVGRenderer } from '@antv/g-svg'; import { Renderer as SVGRenderer } from '@antv/g-svg';
@ -60,12 +59,11 @@ export function createGraphCanvas(
} as unknown as HTMLCanvasElement; } as unknown as HTMLCanvasElement;
const context = new OffscreenCanvasContext(offscreenNodeCanvas); const context = new OffscreenCanvasContext(offscreenNodeCanvas);
const instance = getRenderer(renderer) as any as IRenderer;
return new Canvas({ return new Canvas({
container, container,
width, width,
height, height,
renderer: () => instance, renderer: () => getRenderer(renderer),
...extraOptions, ...extraOptions,
}); });
} }

View File

@ -1,4 +1,5 @@
import type { Graph, IAnimateEvent } from '@/src'; import type { Graph, IAnimateEvent } from '@/src';
import type { CanvasLayer } from '@/src/types';
import type { Canvas, IAnimation } from '@antv/g'; import type { Canvas, IAnimation } from '@antv/g';
import chalk from 'chalk'; import chalk from 'chalk';
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
@ -21,7 +22,7 @@ const formatSVG = (svg: string, keepSVGElementId: boolean) => {
// @see https://jestjs.io/docs/26.x/expect#expectextendmatchers // @see https://jestjs.io/docs/26.x/expect#expectextendmatchers
export async function toMatchSVGSnapshot( export async function toMatchSVGSnapshot(
gCanvas: Canvas | Canvas[], gCanvas: Record<CanvasLayer, Canvas>,
dir: string, dir: string,
name: string, name: string,
options: ToMatchSVGSnapshotOptions = {}, options: ToMatchSVGSnapshotOptions = {},
@ -32,15 +33,15 @@ export async function toMatchSVGSnapshot(
const namePath = join(dir, name); const namePath = join(dir, name);
const actualPath = join(dir, `${name}-actual.${fileFormat}`); const actualPath = join(dir, `${name}-actual.${fileFormat}`);
const expectedPath = join(dir, `${name}.${fileFormat}`); const expectedPath = join(dir, `${name}.${fileFormat}`);
const gCanvases = Array.isArray(gCanvas) ? gCanvas : [gCanvas];
let actual: string = ''; let actual: string = '';
// Clone <svg> // Clone <svg>
const svg = (gCanvases[0].getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement; const svg = (gCanvas.main.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;
const gRoot = svg.querySelector('#g-root'); const gRoot = svg.querySelector('#g-root');
gCanvases.slice(1).forEach((gCanvas) => { Object.entries(gCanvas).forEach(([key, gCanvas]) => {
if (key === 'main') return;
const dom = (gCanvas.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement; const dom = (gCanvas.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;
// @ts-expect-error dom is SVGElement // @ts-expect-error dom is SVGElement
gRoot?.append(...(dom.querySelector('#g-root')?.childNodes || [])); gRoot?.append(...(dom.querySelector('#g-root')?.childNodes || []));
@ -99,7 +100,7 @@ export async function toMatchSnapshot(
detail?: string, detail?: string,
options: ToMatchSVGSnapshotOptions = {}, options: ToMatchSVGSnapshotOptions = {},
) { ) {
return await toMatchSVGSnapshot(Object.values(graph.getCanvas().canvas), ...getSnapshotDir(dir, detail), options); return await toMatchSVGSnapshot(graph.getCanvas().getLayers(), ...getSnapshotDir(dir, detail), options);
} }
export async function toMatchAnimation( export async function toMatchAnimation(
@ -127,7 +128,7 @@ export async function toMatchAnimation(
await sleep(32); await sleep(32);
const result = await toMatchSVGSnapshot( const result = await toMatchSVGSnapshot(
Object.values(graph.getCanvas().canvas), graph.getCanvas().getLayers(),
...getSnapshotDir(dir, `${detail}-${frame}`), ...getSnapshotDir(dir, `${detail}-${frame}`),
options, options,
); );

View File

@ -376,11 +376,10 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
} else { } else {
this.shadow = new Rect({ this.shadow = new Rect({
style: { style: {
$layer: 'transient',
...shadowStyle, ...shadowStyle,
...positionStyle, ...positionStyle,
pointerEvents: 'none', pointerEvents: 'none',
// @ts-expect-error $layer is not in the type definition
$layer: 'transient',
}, },
}); });
this.context.canvas.appendChild(this.shadow); this.context.canvas.appendChild(this.shadow);

View File

@ -1,9 +1,13 @@
import { IAnimation } from '@antv/g'; import type { IAnimation } from '@antv/g';
import { Keyframe } from '../types'; import type { RuntimeContext } from '../runtime/types';
import type { BaseShapeStyleProps } from './shapes'; import type { BaseElementStyleProps, Keyframe } from '../types';
import { BaseShape } from './shapes'; import { BaseShape } from './shapes';
export abstract class BaseElement<T extends BaseShapeStyleProps> extends BaseShape<T> { export abstract class BaseElement<T extends BaseElementStyleProps> extends BaseShape<T> {
protected get context(): RuntimeContext {
return this.attributes.context!;
}
protected get parsedAttributes() { protected get parsedAttributes() {
return this.attributes as Required<T>; return this.attributes as Required<T>;
} }

View File

@ -194,7 +194,7 @@ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStylePr
} }
protected getComboZIndex(attributes: Required<S>): number { protected getComboZIndex(attributes: Required<S>): number {
const ancestors = attributes.context!.model.getAncestorsData(this.id, COMBO_KEY) || []; const ancestors = this.context.model.getAncestorsData(this.id, COMBO_KEY) || [];
return ancestors.length; return ancestors.length;
} }
@ -231,7 +231,7 @@ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStylePr
Object.assign(this.style, comboStyle); Object.assign(this.style, comboStyle);
// Sync combo position to model // Sync combo position to model
const { x, y } = comboStyle; const { x, y } = comboStyle;
attributes.context!.model.syncComboDatum({ id: this.id, style: { x, y } }); this.context.model.syncComboDatum({ id: this.id, style: { x, y } });
} }
public render(attributes: Required<S>, container: Group = this) { public render(attributes: Required<S>, container: Group = this) {

View File

@ -50,7 +50,7 @@ export class HTML extends BaseNode<HTMLStyleProps> {
private rootPointerEvent = new FederatedPointerEvent(null); private rootPointerEvent = new FederatedPointerEvent(null);
private get eventService() { private get eventService() {
return this.attributes.context!.canvas.context.eventService; return this.context.canvas.context.eventService;
} }
private get events() { private get events() {
@ -105,7 +105,7 @@ export class HTML extends BaseNode<HTMLStyleProps> {
} }
private forwardEvents = (nativeEvent: PointerEvent) => { private forwardEvents = (nativeEvent: PointerEvent) => {
const canvas = this.attributes.context!.canvas.main; const canvas = this.context.canvas;
const iCanvas = canvas.context.renderingContext.root!.ownerDocument!.defaultView!; const iCanvas = canvas.context.renderingContext.root!.ownerDocument!.defaultView!;
const normalizedEvents = this.normalizeToPointerEvent(nativeEvent, iCanvas); const normalizedEvents = this.normalizeToPointerEvent(nativeEvent, iCanvas);
@ -232,11 +232,7 @@ export class HTML extends BaseNode<HTMLStyleProps> {
let x: number; let x: number;
let y: number; let y: number;
const { offsetX, offsetY, clientX, clientY } = nativeEvent; const { offsetX, offsetY, clientX, clientY } = nativeEvent;
if ( if (this.context.canvas.context.config.supportsCSSTransform && !isNil(offsetX) && !isNil(offsetY)) {
this.attributes.context?.canvas.main.context.config.supportsCSSTransform &&
!isNil(offsetX) &&
!isNil(offsetY)
) {
x = offsetX; x = offsetX;
y = offsetY; y = offsetY;
} else { } else {

12
packages/g6/src/global.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import '@antv/g';
declare module '@antv/g' {
interface BaseStyleProps {
/**
* <zh/> 'main'
*
* <en/> The layer where the shape is located, default is 'main'.
*/
$layer?: string;
}
}

View File

@ -58,7 +58,10 @@ export class Fullscreen extends BasePlugin<FullscreenOptions> {
private shortcut: Shortcut; private shortcut: Shortcut;
private style: HTMLStyleElement;
private $el = this.context.canvas.getContainer()!; private $el = this.context.canvas.getContainer()!;
private graphSize: [number, number] = [0, 0]; private graphSize: [number, number] = [0, 0];
constructor(context: RuntimeContext, options: FullscreenOptions) { constructor(context: RuntimeContext, options: FullscreenOptions) {
@ -68,7 +71,13 @@ export class Fullscreen extends BasePlugin<FullscreenOptions> {
this.bindEvents(); this.bindEvents();
this.$el.style.backgroundColor = this.context.graph.getBackground()!; this.style = document.createElement('style');
document.head.appendChild(this.style);
this.style.innerHTML = `
:not(:root):fullscreen::backdrop {
background: transparent;
}
`;
} }
private bindEvents() { private bindEvents() {
@ -150,6 +159,12 @@ export class Fullscreen extends BasePlugin<FullscreenOptions> {
super.update(options); super.update(options);
this.bindEvents(); this.bindEvents();
} }
public destroy(): void {
this.exit();
this.style.remove();
super.destroy();
}
} }
/** /**

View File

@ -1,17 +1,15 @@
import type { Cursor, DisplayObject, CanvasConfig as GCanvasConfig, IAnimation, IRenderer } from '@antv/g'; import type { DisplayObject, CanvasConfig as GCanvasConfig, IChildNode } from '@antv/g';
import { CanvasEvent, Canvas as GCanvas } from '@antv/g'; import { CanvasEvent, Canvas as GCanvas } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop'; import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';
import { createDOM, isFunction, isString } from '@antv/util'; import { createDOM } from '@antv/util';
import type { CanvasOptions } from '../spec/canvas'; import type { CanvasOptions } from '../spec/canvas';
import type { PointObject } from '../types'; import type { CanvasLayer } from '../types';
import type { CanvasLayer } from '../types/canvas';
import { getBBoxSize, getCombinedBBox } from '../utils/bbox'; import { getBBoxSize, getCombinedBBox } from '../utils/bbox';
export interface CanvasConfig export interface CanvasConfig
extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor'> { extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor' | 'background'> {
renderer?: CanvasOptions['renderer']; renderer?: CanvasOptions['renderer'];
background?: string;
} }
export interface DataURLOptions { export interface DataURLOptions {
@ -40,153 +38,93 @@ export interface DataURLOptions {
encoderOptions: number; encoderOptions: number;
} }
/** const layersName: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
* @deprecated this canvas will be replace by layered canvas
*/
export class Canvas {
protected config: CanvasConfig;
public background!: GCanvas; export class Canvas extends GCanvas {
public main!: GCanvas; private extends: {
public label!: GCanvas; config: CanvasConfig;
public transient!: GCanvas; renderer: CanvasOptions['renderer'];
renderers: Record<CanvasLayer, CanvasRenderer>;
layers: Record<CanvasLayer, GCanvas>;
};
public get canvas() { public getLayer(layer: CanvasLayer) {
return { return this.extends.layers[layer];
main: this.main, }
label: this.label,
transient: this.transient, /**
background: this.background, * <zh/>
*
* <en/> Get all layers
* @returns <zh/> <en/> Layer
*/
public getLayers() {
return this.extends.layers;
}
/**
* <zh/>
*
* <en/> Get renderer
* @param layer - <zh/> <en/> Layer
* @returns <zh/> <en/> Renderer
*/
public getRenderer(layer: CanvasLayer) {
return this.extends.renderers[layer];
}
constructor(config: CanvasConfig) {
const { renderer, background, cursor, ...restConfig } = config;
const renderers = createRenderers(renderer);
// init main canvas
super({ ...restConfig, supportsMutipleCanvasesInOneContainer: true, cursor, renderer: renderers.main });
const layers = Object.fromEntries(
layersName.map((layer) => {
if (layer === 'main') return [layer, this];
const canvas = new GCanvas({
...restConfig,
supportsMutipleCanvasesInOneContainer: true,
renderer: renderers[layer],
background: layer === 'background' ? background : undefined,
});
return [layer, canvas];
}),
) as Record<CanvasLayer, GCanvas>;
configCanvasDom(layers);
this.extends = {
config,
renderer,
renderers,
layers,
}; };
} }
public get document() { public get ready() {
return this.main.document; return Promise.all([
} super.ready,
...Object.entries(this.getLayers()).map(([layer, canvas]) =>
public renderers!: Record<CanvasLayer, IRenderer>; layer === 'main' ? Promise.resolve() : canvas.ready,
),
private initialized = false; ]);
constructor(config: CanvasConfig) {
this.config = config;
}
public async init() {
if (this.initialized) return;
const { renderer: getRenderer, background, ...restConfig } = this.config;
const names: CanvasLayer[] = ['main', 'label', 'transient', 'background'];
const { renderers, canvas } = names.reduce(
(acc, name) => {
const renderer = isFunction(getRenderer) ? getRenderer?.(name) : new CanvasRenderer();
if (name === 'main') {
renderer.registerPlugin(
new DragNDropPlugin({
isDocumentDraggable: true,
isDocumentDroppable: true,
dragstartDistanceThreshold: 10,
dragstartTimeThreshold: 100,
}),
);
} else {
renderer.unregisterPlugin(renderer.getPlugin('dom-interaction'));
}
const canvas = new GCanvas({
renderer,
supportsMutipleCanvasesInOneContainer: true,
...restConfig,
});
acc.renderers[name] = renderer;
acc.canvas[name] = canvas;
this[name] = canvas;
return acc;
},
{ renderers: {}, canvas: {} } as {
renderers: Record<CanvasLayer, IRenderer>;
canvas: Record<CanvasLayer, GCanvas>;
},
);
this.renderers = renderers;
Object.entries(canvas).forEach(([name, canvas]) => {
const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;
domElement.style.position = 'absolute';
domElement.style.outline = 'none';
domElement.tabIndex = 1;
if (name !== 'main') domElement.style.pointerEvents = 'none';
});
this.setBackground();
await Promise.all(Object.values(this.canvas).map((canvas) => canvas.ready));
this.initialized = true;
}
public getRendererType(layer: CanvasLayer = 'main') {
const plugins = this.renderers[layer].getPlugins();
for (const plugin of plugins) {
if (plugin.name === 'canvas-renderer') return 'canvas';
if (plugin.name === 'svg-renderer') return 'svg';
if (plugin.name === 'device-renderer') return 'gpu';
}
return 'unknown';
}
public get context() {
return this.main.context;
}
public getDevice() {
// @ts-expect-error deviceRendererPlugin is private
return this.main.context?.deviceRendererPlugin?.getDevice();
}
public getConfig() {
return this.config;
}
public setBackground(background = this.config.background) {
this.config.background = background;
}
public setCursor(cursor: Cursor) {
this.config.cursor = cursor;
Object.values(this.canvas).forEach((canvas) => {
canvas.setCursor(cursor);
});
}
public getSize(): [number, number] {
return [this.config.width || 0, this.config.height || 0];
} }
public resize(width: number, height: number) { public resize(width: number, height: number) {
this.config.width = width; Object.assign(this.extends.config, { width, height });
this.config.height = height; Object.values(this.getLayers()).forEach((canvas) => {
Object.values(this.canvas).forEach((canvas) => { if (canvas === this) super.resize(width, height);
canvas.resize(width, height); else canvas.resize(width, height);
}); });
} }
public getCamera() {
return this.main.getCamera();
}
public getBounds() { public getBounds() {
return getCombinedBBox( return getCombinedBBox(
Object.values(this.canvas) Object.values(this.getLayers())
.map((canvas) => canvas.document.documentElement) .map((canvas) => canvas.document.documentElement)
.filter((el) => el.childNodes.length > 0) .filter((el) => el.childNodes.length > 0)
.map((el) => el.getBounds()), .map((el) => el.getBounds()),
@ -194,48 +132,38 @@ export class Canvas {
} }
public getContainer() { public getContainer() {
const container = this.config.container!; const container = this.extends.config.container!;
return typeof container === 'string' ? document.getElementById(container!) : container;
return isString(container) ? document.getElementById(container!) : container;
} }
public appendChild<T extends DisplayObject>(child: T): T { public getSize(): [number, number] {
const layer = (child.style?.$layer || 'main') as CanvasLayer; return [this.extends.config.width || 0, this.extends.config.height || 0];
return this[layer].appendChild(child);
} }
public getContextService() { public appendChild<T extends IChildNode>(child: T, index?: number): T {
return this.main.getContextService(); const layer = ((child as unknown as DisplayObject).style?.$layer || 'main') as CanvasLayer;
if (layer === 'main') return super.appendChild(child, index);
return this.getLayer(layer).appendChild(child, index);
} }
public viewport2Client(viewport: PointObject) { public setRenderer(renderer: any) {
return this.main.viewport2Client(viewport); if (renderer === this.extends.renderer) return;
} const renderers = createRenderers(renderer);
this.extends.renderers = renderers;
public viewport2Canvas(viewport: PointObject) { Object.entries(renderers).forEach(([layer, instance]) => {
return this.main.viewport2Canvas(viewport); if (layer === 'main') super.setRenderer(instance);
} else this.getLayer(layer as CanvasLayer).setRenderer(instance);
});
public client2Viewport(client: PointObject) {
return this.main.client2Viewport(client);
}
public canvas2Viewport(canvas: PointObject) {
return this.main.canvas2Viewport(canvas);
} }
public async toDataURL(options: Partial<DataURLOptions> = {}) { public async toDataURL(options: Partial<DataURLOptions> = {}) {
const devicePixelRatio = window.devicePixelRatio || 1; const devicePixelRatio = window.devicePixelRatio || 1;
const { mode = 'viewport', ...restOptions } = options; const { mode = 'viewport', ...restOptions } = options;
let [startX, startY, width, height] = [0, 0, 0, 0];
let startX = 0;
let startY = 0;
let width = 0;
let height = 0;
if (mode === 'viewport') { if (mode === 'viewport') {
[width, height] = [this.config.width || 0, this.config.height || 0]; [width, height] = this.getSize();
} else if (mode === 'overall') { } else if (mode === 'overall') {
const bounds = this.getBounds(); const bounds = this.getBounds();
const size = getBBoxSize(bounds); const size = getBBoxSize(bounds);
@ -251,28 +179,28 @@ export class Canvas {
renderer: new CanvasRenderer(), renderer: new CanvasRenderer(),
devicePixelRatio, devicePixelRatio,
container, container,
background: this.config.background, background: this.extends.config.background,
}); });
await offscreenCanvas.ready; await offscreenCanvas.ready;
offscreenCanvas.appendChild(this.background.getRoot().cloneNode(true)); offscreenCanvas.appendChild(this.getLayer('background').getRoot().cloneNode(true));
offscreenCanvas.appendChild(this.main.getRoot().cloneNode(true)); offscreenCanvas.appendChild(this.getRoot().cloneNode(true));
// Handle label canvas // Handle label canvas
const label = this.label.getRoot().cloneNode(true); const label = this.getLayer('label').getRoot().cloneNode(true);
const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 }); const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 });
const currentCanvasPosition = this.main.viewport2Canvas({ x: 0, y: 0 }); const currentCanvasPosition = this.viewport2Canvas({ x: 0, y: 0 });
label.translate([ label.translate([
currentCanvasPosition.x - originCanvasPosition.x, currentCanvasPosition.x - originCanvasPosition.x,
currentCanvasPosition.y - originCanvasPosition.y, currentCanvasPosition.y - originCanvasPosition.y,
]); ]);
label.scale(1 / this.main.getCamera().getZoom()); label.scale(1 / this.getCamera().getZoom());
offscreenCanvas.appendChild(label); offscreenCanvas.appendChild(label);
offscreenCanvas.appendChild(this.transient.getRoot().cloneNode(true)); offscreenCanvas.appendChild(this.getLayer('transient').getRoot().cloneNode(true));
const camera = this.main.getCamera(); const camera = this.getCamera();
const offscreenCamera = offscreenCanvas.getCamera(); const offscreenCamera = offscreenCanvas.getCamera();
if (mode === 'viewport') { if (mode === 'viewport') {
@ -289,7 +217,7 @@ export class Canvas {
const contextService = offscreenCanvas.getContextService(); const contextService = offscreenCanvas.getContextService();
return new Promise<string>((resolve) => { return new Promise<string>((resolve) => {
offscreenCanvas.on(CanvasEvent.RERENDER, async () => { offscreenCanvas.addEventListener(CanvasEvent.RERENDER, async () => {
// 等待图片渲染完成 / Wait for the image to render // 等待图片渲染完成 / Wait for the image to render
await new Promise((r) => setTimeout(r, 300)); await new Promise((r) => setTimeout(r, 300));
const url = await contextService.toDataURL(restOptions); const url = await contextService.toDataURL(restOptions);
@ -298,57 +226,60 @@ export class Canvas {
}); });
} }
public destroy() { public destroy(cleanUp?: boolean, skipTriggerEvent?: boolean) {
this.config = {}; Object.values(this.getLayers()).forEach((canvas) => {
// @ts-expect-error force delete
this.renderers = {};
Object.entries(this.canvas).forEach(([name, canvas]) => {
const camera = canvas.getCamera(); const camera = canvas.getCamera();
// @ts-expect-error landmark is private camera.cancelLandmarkAnimation();
if (camera.landmarks?.length) { if (canvas === this) super.destroy(cleanUp, skipTriggerEvent);
camera.cancelLandmarkAnimation(); else canvas.destroy(cleanUp, skipTriggerEvent);
}
destroyCanvas(canvas);
// @ts-expect-error force delete
this[name] = undefined;
}); });
} }
} }
/** /**
* <zh/> G Canvas destroy * <zh/>
* *
* <en/> G Canvas destroy does not handle animation objects, causing memory leaks * <en/> Create renderers
* @param canvas GCanvas * @param renderer - <zh/> <en/> Renderer creator
* @remarks * @returns <zh/> <en/> Renderer
* <zh/> G
* 1020% 2800MB 2200MB
*
* <en/> These operations should be completed in G, this is just a temporary solution
* This operation can reduce memory usage by 10-20% in the test environment (from 2800MB to 2200MB)
*/ */
function destroyCanvas(canvas: GCanvas) { function createRenderers(renderer: CanvasConfig['renderer']) {
canvas.destroy(); return Object.fromEntries(
layersName.map((layer) => {
const instance = renderer?.(layer) || new CanvasRenderer();
// 移除相机事件 / Remove camera events if (layer === 'main') {
const camera = canvas.getCamera(); instance.registerPlugin(
camera.eventEmitter.removeAllListeners(); new DragNDropPlugin({
isDocumentDraggable: true,
isDocumentDroppable: true,
dragstartDistanceThreshold: 10,
dragstartTimeThreshold: 100,
}),
);
} else {
instance.unregisterPlugin(instance.getPlugin('dom-interaction'));
}
canvas.document.timeline.destroy(); return [layer, instance];
// @ts-expect-error private property }),
const { animationsWithPromises } = canvas.document.timeline; ) as Record<CanvasLayer, CanvasRenderer>;
// 释放 target 对象图形 / Release target object graphics }
animationsWithPromises.forEach((animation: IAnimation) => {
if (animation.effect.target) animation.effect.target = null; /**
// @ts-expect-error private property * <zh/> DOM
if (animation.effect.computedTiming) animation.effect.computedTiming = null; *
}); * <en/> Configure canvas DOM
* @param layers - <zh/> <en/> Canvas
// @ts-expect-error private property */
canvas.document.timeline.animationsWithPromises = []; function configCanvasDom(layers: Record<CanvasLayer, GCanvas>) {
// @ts-expect-error private property Object.entries(layers).forEach(([layer, canvas]) => {
canvas.document.timeline.rafCallbacks = []; const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;
// @ts-expect-error private property
canvas.document.timeline = null; domElement.style.position = 'absolute';
domElement.style.outline = 'none';
domElement.tabIndex = 1;
if (layer !== 'main') domElement.style.pointerEvents = 'none';
});
} }

View File

@ -3,7 +3,6 @@ import type { AABB, BaseStyleProps } from '@antv/g';
import { debounce, isEqual, isFunction, isNumber, isObject, isString, omit } from '@antv/util'; import { debounce, isEqual, isFunction, isNumber, isObject, isString, omit } from '@antv/util';
import { COMBO_KEY, GraphEvent } from '../constants'; import { COMBO_KEY, GraphEvent } from '../constants';
import type { Plugin } from '../plugins/types'; import type { Plugin } from '../plugins/types';
import { getExtension } from '../registry';
import type { import type {
BehaviorOptions, BehaviorOptions,
ComboData, ComboData,
@ -119,12 +118,10 @@ export class Graph extends EventEmitter {
* @apiCategory option * @apiCategory option
*/ */
public setOptions(options: GraphOptions): void { public setOptions(options: GraphOptions): void {
const { background, behaviors, combo, data, edge, height, layout, node, plugins, theme, transforms, width } = const { behaviors, combo, data, edge, height, layout, node, plugins, theme, transforms, width, renderer } = options;
options;
Object.assign(this.options, options); Object.assign(this.options, options);
if (background) this.setBackground(background);
if (behaviors) this.setBehaviors(behaviors); if (behaviors) this.setBehaviors(behaviors);
if (combo) this.setCombo(combo); if (combo) this.setCombo(combo);
if (data) this.setData(data); if (data) this.setData(data);
@ -136,29 +133,7 @@ export class Graph extends EventEmitter {
if (transforms) this.setTransforms(transforms); if (transforms) this.setTransforms(transforms);
if (isNumber(width) || isNumber(height)) if (isNumber(width) || isNumber(height))
this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0); this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0);
} if (renderer) this.context.canvas.setRenderer(renderer);
/**
* <zh/>
*
* <en/> Set canvas background color
* @param background - <zh/> | <en/> background color
* @apiCategory canvas
*/
public setBackground(background: GraphOptions['background']): void {
this.options.background = background;
this.context.canvas?.setBackground(background);
}
/**
* <zh/>
*
* <en/> Get canvas background color
* @returns <zh/> | <en/> background color
* @apiCategory canvas
*/
public getBackground(): GraphOptions['background'] {
return this.options.background;
} }
/** /**
@ -280,11 +255,6 @@ export class Graph extends EventEmitter {
*/ */
public setTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void { public setTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void {
this.options.theme = isFunction(theme) ? theme(this.getTheme()) : theme; this.options.theme = isFunction(theme) ? theme(this.getTheme()) : theme;
const { background } = getExtension('theme', this.options.theme) || {};
if (background && !this.options.background) {
this.setBackground(background);
}
} }
/** /**
@ -1034,14 +1004,13 @@ export class Graph extends EventEmitter {
} }
private async initCanvas() { private async initCanvas() {
if (this.context.canvas) return await this.context.canvas.init(); if (this.context.canvas) return await this.context.canvas.ready;
const { container = 'container', width, height, renderer, background } = this.options; const { container = 'container', width, height, renderer, background } = this.options;
if (container instanceof Canvas) { if (container instanceof Canvas) {
this.context.canvas = container; this.context.canvas = container;
container.setBackground(background); await container.ready;
await container.init();
} else { } else {
const $container = isString(container) ? document.getElementById(container!) : container; const $container = isString(container) ? document.getElementById(container!) : container;
const containerSize = sizeOf($container!); const containerSize = sizeOf($container!);
@ -1057,7 +1026,7 @@ export class Graph extends EventEmitter {
}); });
this.context.canvas = canvas; this.context.canvas = canvas;
await canvas.init(); await canvas.ready;
this.emit(GraphEvent.AFTER_CANVAS_INIT, { canvas }); this.emit(GraphEvent.AFTER_CANVAS_INIT, { canvas });
} }
} }

View File

@ -1,2 +1,2 @@
packages: packages:
- 'packages/**' - 'packages/*'