@antv/g6
Version:
A Graph Visualization Framework in JavaScript
412 lines (354 loc) • 12.7 kB
text/typescript
import type { Cursor, DisplayObject, CanvasConfig as GCanvasConfig, IChildNode } from '@antv/g';
import { CanvasEvent, Canvas as GCanvas } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';
import { createDOM } from '@antv/util';
import type { CanvasOptions } from '../spec/canvas';
import type { CanvasLayer, Point } from '../types';
import { getBBoxSize, getCombinedBBox } from '../utils/bbox';
import { parsePoint, toPointObject } from '../utils/point';
export interface CanvasConfig
extends Pick<GCanvasConfig, 'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor' | 'background'> {
/**
* <zh/> 渲染器
*
* <en/> renderer
*/
renderer?: CanvasOptions['renderer'];
/**
* <zh/> 是否启用多图层
*
* <en/> Whether to enable multiple layers
* @defaultValue true
* @remarks
* <zh/> 非动态参数,仅在初始化时生效
*
* <en/> Non-dynamic parameters, only take effect during initialization
*/
enableMultiLayer?: boolean;
}
export interface DataURLOptions {
/**
* <zh/> 导出模式
* - viewport: 导出视口内容
* - overall: 导出整个画布
*
* <en/> export mode
* - viewport: export the content of the viewport
* - overall: export the entire canvas
*/
mode?: 'viewport' | 'overall';
/**
* <zh/> 图片类型
*
* <en/> image type
* @defaultValue 'image/png'
*/
type: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/bmp';
/**
* <zh/> 图片质量, 仅对 image/jpeg 和 image/webp 有效,取值范围 0 ~ 1
*
* <en/> image quality, only valid for image/jpeg and image/webp, range 0 ~ 1
*/
encoderOptions: number;
}
const SINGLE_LAYER_NAME: CanvasLayer[] = ['main'];
const MULTI_LAYER_NAME: CanvasLayer[] = ['background', 'main', 'label', 'transient'];
/**
* <zh/> 获取主画布图层
*
* <en/> Get the main canvas layer
* @param layers - <zh/> 画布图层 | <en/> Canvas layer
* @returns <zh/> 主画布图层 | <en/> Main canvas layer
*/
function getMainLayerOf(layers: Record<CanvasLayer, GCanvas>) {
return layers.main;
}
export class Canvas {
private extends: {
config: CanvasConfig;
renderer: CanvasOptions['renderer'];
renderers: Record<CanvasLayer, CanvasRenderer>;
layers: Record<CanvasLayer, GCanvas>;
};
private config: CanvasConfig = {
enableMultiLayer: true,
};
public getConfig() {
return this.config;
}
public getLayer(layer: CanvasLayer = 'main') {
return this.extends.layers[layer] || getMainLayerOf(this.getLayers());
}
/**
* <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];
}
/**
* <zh/> 获取相机
*
* <en/> Get camera
* @param layer - <zh/> 图层 <en/> Layer
* @returns <zh/> 相机 <en/> Camera
*/
public getCamera(layer: CanvasLayer = 'main') {
return this.getLayer(layer).getCamera();
}
public getRoot(layer: CanvasLayer = 'main') {
return this.getLayer(layer).getRoot();
}
public getContextService(layer: CanvasLayer = 'main') {
return this.getLayer(layer).getContextService();
}
public setCursor(cursor: Cursor): void {
this.config.cursor = cursor;
this.getLayer().setCursor(cursor);
}
public get document() {
return this.getLayer().document;
}
public get context() {
return this.getLayer().context;
}
constructor(config: CanvasConfig) {
Object.assign(this.config, config);
const { renderer, background, cursor, enableMultiLayer, ...restConfig } = this.config;
const layersName = enableMultiLayer ? MULTI_LAYER_NAME : SINGLE_LAYER_NAME;
const renderers = createRenderers(renderer, layersName);
const layers = Object.fromEntries(
layersName.map((layer) => {
const canvas = new GCanvas({
...restConfig,
supportsMutipleCanvasesInOneContainer: enableMultiLayer,
renderer: renderers[layer],
background: enableMultiLayer ? (layer === 'background' ? background : undefined) : background,
});
return [layer, canvas];
}),
) as Record<CanvasLayer, GCanvas>;
configCanvasDom(layers);
this.extends = {
config: this.config,
renderer,
renderers,
layers,
};
}
public get ready() {
return Promise.all(Object.entries(this.getLayers()).map(([, canvas]) => canvas.ready));
}
public resize(width: number, height: number) {
Object.assign(this.extends.config, { width, height });
Object.values(this.getLayers()).forEach((canvas) => {
const camera = canvas.getCamera();
const position = camera.getPosition();
const focalPoint = camera.getFocalPoint();
canvas.resize(width, height);
camera.setPosition(position);
camera.setFocalPoint(focalPoint);
});
}
/**
* <zh/> 获取画布边界
*
* <en/> Get canvas boundary
* @param group
* <zh/> 元素分组
* - undefined: 获取整个画布边界
* - 'elements': 仅获取元素边界
* - 'plugins': 仅获取插件边界
*
* <en/> Element group
* - undefined: Get the entire canvas boundary
* - 'elements': Get only the element boundary
* - 'plugins': Get only the plugin boundary
* @returns <zh/> 边界 <en/> Boundary
*/
public getBounds(group?: 'elements' | 'plugins') {
return getCombinedBBox(
Object.values(this.getLayers())
.map((canvas) => {
const g = group
? (canvas
.getRoot()
.childNodes.find((node) => (node as DisplayObject).classList.includes(group)) as DisplayObject)
: canvas.getRoot();
return g;
})
.filter((el) => el?.childNodes.length > 0)
.map((el) => el.getBounds()),
);
}
public getContainer() {
const container = this.extends.config.container!;
return typeof container === 'string' ? document.getElementById(container!) : container;
}
public getSize(): [number, number] {
return [this.extends.config.width || 0, this.extends.config.height || 0];
}
public appendChild<T extends IChildNode>(child: T, index?: number): T {
const layer = ((child as unknown as DisplayObject).style?.$layer || 'main') as CanvasLayer;
return this.getLayer(layer).appendChild(child, index);
}
public setRenderer(renderer: CanvasOptions['renderer']) {
if (renderer === this.extends.renderer) return;
const renderers = createRenderers(renderer, this.config.enableMultiLayer ? MULTI_LAYER_NAME : SINGLE_LAYER_NAME);
this.extends.renderers = renderers;
Object.entries(renderers).forEach(([layer, instance]) => this.getLayer(layer as CanvasLayer).setRenderer(instance));
configCanvasDom(this.getLayers());
}
public getCanvasByViewport(point: Point): Point {
return parsePoint(this.getLayer().viewport2Canvas(toPointObject(point)));
}
public getViewportByCanvas(point: Point): Point {
return parsePoint(this.getLayer().canvas2Viewport(toPointObject(point)));
}
public getViewportByClient(point: Point): Point {
return parsePoint(this.getLayer().client2Viewport(toPointObject(point)));
}
public getClientByViewport(point: Point): Point {
return parsePoint(this.getLayer().viewport2Client(toPointObject(point)));
}
public getClientByCanvas(point: Point): Point {
return this.getClientByViewport(this.getViewportByCanvas(point));
}
public getCanvasByClient(point: Point): Point {
const main = this.getLayer();
const viewportPoint = main.client2Viewport(toPointObject(point));
return parsePoint(main.viewport2Canvas(viewportPoint));
}
public async toDataURL(options: Partial<DataURLOptions> = {}) {
const devicePixelRatio = globalThis.devicePixelRatio || 1;
const { mode = 'viewport', ...restOptions } = options;
let [startX, startY, width, height] = [0, 0, 0, 0];
if (mode === 'viewport') {
[width, height] = this.getSize();
} else if (mode === 'overall') {
const bounds = this.getBounds();
const size = getBBoxSize(bounds);
[startX, startY] = bounds.min;
[width, height] = size;
}
const container: HTMLElement = createDOM('<div id="virtual-image"></div>');
const offscreenCanvas = new GCanvas({
width,
height,
renderer: new CanvasRenderer(),
devicePixelRatio,
container,
background: this.extends.config.background,
});
await offscreenCanvas.ready;
offscreenCanvas.appendChild(this.getLayer('background').getRoot().cloneNode(true));
offscreenCanvas.appendChild(this.getRoot().cloneNode(true));
// Handle label canvas
const label = this.getLayer('label').getRoot().cloneNode(true);
const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 });
const currentCanvasPosition = this.getCanvasByViewport([0, 0]);
label.translate([
currentCanvasPosition[0] - originCanvasPosition.x,
currentCanvasPosition[1] - originCanvasPosition.y,
]);
label.scale(1 / this.getCamera().getZoom());
offscreenCanvas.appendChild(label);
offscreenCanvas.appendChild(this.getLayer('transient').getRoot().cloneNode(true));
const camera = this.getCamera();
const offscreenCamera = offscreenCanvas.getCamera();
if (mode === 'viewport') {
offscreenCamera.setZoom(camera.getZoom());
offscreenCamera.setPosition(camera.getPosition());
offscreenCamera.setFocalPoint(camera.getFocalPoint());
} else if (mode === 'overall') {
const [x, y, z] = offscreenCamera.getPosition();
const [fx, fy, fz] = offscreenCamera.getFocalPoint();
offscreenCamera.setPosition([x + startX, y + startY, z]);
offscreenCamera.setFocalPoint([fx + startX, fy + startY, fz]);
}
const contextService = offscreenCanvas.getContextService();
return new Promise<string>((resolve) => {
offscreenCanvas.addEventListener(CanvasEvent.RERENDER, async () => {
// 等待图片渲染完成 / Wait for the image to render
await new Promise((r) => setTimeout(r, 300));
const url = await contextService.toDataURL(restOptions);
resolve(url);
});
});
}
public destroy() {
Object.values(this.getLayers()).forEach((canvas) => {
const camera = canvas.getCamera();
camera.cancelLandmarkAnimation();
canvas.destroy();
});
}
}
/**
* <zh/> 创建渲染器
*
* <en/> Create renderers
* @param renderer - <zh/> 渲染器创建器 <en/> Renderer creator
* @param layersName - <zh/> 图层名称 <en/> Layer name
* @returns <zh/> 渲染器 <en/> Renderer
*/
function createRenderers(renderer: CanvasConfig['renderer'], layersName: CanvasLayer[]) {
return Object.fromEntries(
layersName.map((layer) => {
const instance = renderer?.(layer) || new CanvasRenderer();
if (instance instanceof CanvasRenderer) {
instance.setConfig({ enableDirtyRectangleRendering: false });
}
if (layer === 'main') {
instance.registerPlugin(
new DragNDropPlugin({
isDocumentDraggable: true,
isDocumentDroppable: true,
dragstartDistanceThreshold: 10,
dragstartTimeThreshold: 100,
}),
);
} else {
instance.unregisterPlugin(instance.getPlugin('dom-interaction'));
}
return [layer, instance];
}),
) as Record<CanvasLayer, CanvasRenderer>;
}
/**
* <zh/> 配置画布 DOM
*
* <en/> Configure canvas DOM
* @param layers - <zh/> 画布 <en/> Canvas
*/
function configCanvasDom(layers: Record<CanvasLayer, GCanvas>) {
Object.entries(layers).forEach(([layer, canvas]) => {
const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;
// 浏览器环境下,设置画布样式
// Set canvas style in browser environment
if (domElement?.style) {
domElement.style.gridArea = '1 / 1 / 2 / 2';
domElement.style.outline = 'none';
domElement.tabIndex = 1;
if (layer !== 'main') domElement.style.pointerEvents = 'none';
}
if (domElement?.parentElement) {
domElement.parentElement.style.display = 'grid';
// 给父元素设置独立的层叠上下文,避免外部元素影响内部的层叠逻辑
domElement.parentElement.style.isolation = 'isolate';
}
});
}