@antv/g6
Version:
A Graph Visualization Framework in JavaScript
336 lines (304 loc) • 11.4 kB
text/typescript
import type { BaseStyleProps, DisplayObject, DisplayObjectConfig, Group, IAnimation } from '@antv/g';
import { CustomElement } from '@antv/g';
import { isEmpty, isFunction, upperFirst } from '@antv/util';
import { ExtensionCategory } from '../../constants';
import type { Keyframe } from '../../types';
import { createAnimationsProxy, preprocessKeyframes } from '../../utils/animation';
import { setAttributes, updateStyle } from '../../utils/element';
import { subObject } from '../../utils/prefix';
import { format } from '../../utils/print';
import { getSubShapeStyle } from '../../utils/style';
import { replaceTranslateInTransform } from '../../utils/transform';
import { setVisibility } from '../../utils/visibility';
import { getExtension } from './../../registry/get';
export interface BaseShapeStyleProps extends BaseStyleProps {}
/**
* <zh/> 图形基类
*
* <en/> Base class for shapes
*/
export abstract class BaseShape<StyleProps extends BaseShapeStyleProps> extends CustomElement<StyleProps> {
constructor(options: DisplayObjectConfig<StyleProps>) {
applyTransform(options.style);
super(options);
this.render(this.attributes as Required<StyleProps>, this);
this.setVisibility();
this.bindEvents();
}
/**
* <zh/> 解析后的属性
*
* <en/> parsed attributes
* @returns <zh/> 解析后的属性 | <en/> parsed attributes
* @internal
*/
protected get parsedAttributes() {
return this.attributes as Required<StyleProps>;
}
/**
* <zh/> 图形实例映射表
*
* <en/> shape instance map
* @internal
*/
protected shapeMap: Record<string, DisplayObject> = {};
/**
* <zh/> 动画实例映射表
*
* <en/> animation instance map
* @internal
*/
protected animateMap: Record<string, IAnimation> = {};
/**
* <zh/> 创建、更新或删除图形
*
* <en/> create, update or remove shape
* @param className - <zh/> 图形名称 | <en/> shape name
* @param Ctor - <zh/> 图形类型 | <en/> shape type
* @param style - <zh/> 图形样式。若要删除图形,传入 false | <en/> shape style. Pass false to remove the shape
* @param container - <zh/> 容器 | <en/> container
* @param hooks - <zh/> 钩子函数 | <en/> hooks
* @returns <zh/> 图形实例 | <en/> shape instance
*/
protected upsert<T extends DisplayObject>(
className: string,
Ctor: string | { new (...args: any[]): T },
style: T['attributes'] | false,
container: DisplayObject,
hooks?: UpsertHooks,
): T | undefined {
const target = this.shapeMap[className] as T | undefined;
// remove
// 如果 style 为 false,则删除图形 / remove shape if style is false
if (style === false) {
if (target) {
hooks?.beforeDestroy?.(target);
container.removeChild(target);
delete this.shapeMap[className];
hooks?.afterDestroy?.(target);
}
return;
}
const _Ctor = typeof Ctor === 'string' ? getExtension(ExtensionCategory.SHAPE, Ctor) : Ctor;
if (!_Ctor) {
throw new Error(format(`Shape ${Ctor} not found`));
}
// create
if (!target || target.destroyed || !(target instanceof _Ctor)) {
if (target) {
hooks?.beforeDestroy?.(target);
target?.destroy();
hooks?.afterDestroy?.(target);
}
hooks?.beforeCreate?.();
const instance = new _Ctor({ className, style });
container.appendChild(instance);
this.shapeMap[className] = instance;
hooks?.afterCreate?.(instance);
return instance as T;
}
// update
hooks?.beforeUpdate?.(target);
updateStyle(target, style);
hooks?.afterUpdate?.(target);
return target;
}
public update(attr: Partial<StyleProps> = {}): void {
const attributes = Object.assign({}, this.attributes, attr) as Required<StyleProps>;
applyTransform(attributes);
setAttributes(this, attributes);
this.render(attributes, this);
this.setVisibility();
}
/**
* <zh/> 在初始化时会被自动调用
*
* <en/> will be called automatically when initializing
* @param attributes
* @param container
*/
public abstract render(attributes: Required<StyleProps>, container: Group): void;
public bindEvents() {}
/**
* <zh/> 从给定的属性对象中提取图形样式属性。删除特定的属性,如位置、变换和类名
*
* <en/> Extracts the shape styles from a given attribute object.
* Removes specific styles like position, transformation, and class name.
* @param style - <zh/> 属性对象 | <en/> attribute object
* @returns <zh/> 仅包含样式属性的对象 | <en/> An object containing only the style properties.
*/
public getGraphicStyle<T extends Record<string, any>>(
style: T,
): Omit<T, 'x' | 'y' | 'z' | 'transform' | 'transformOrigin' | 'className' | 'class' | 'zIndex' | 'visibility'> {
return getSubShapeStyle(style);
}
/**
* Get the prefix pairs for composite shapes used to handle animation
* @returns tuples array where each tuple contains a key corresponding to a method `get${key}Style` and its shape prefix
* @internal
*/
protected get compositeShapes(): [string, string][] {
return [
['badges', 'badge-'],
['ports', 'port-'],
];
}
public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions): IAnimation | null {
if (keyframes.length === 0) return null;
const animationMap: IAnimation[] = [];
// 如果 keyframes 中存在 x/y/z ,替换为 transform
// if x/y/z exists in keyframes, replace them with transform
if (keyframes[0].x !== undefined || keyframes[0].y !== undefined || keyframes[0].z !== undefined) {
const { x: _x = 0, y: _y = 0, z: _z = 0 } = this.attributes as Record<string, any>;
keyframes.forEach((keyframe) => {
const { x = _x, y = _y, z = _z } = keyframe;
Object.assign(keyframe, { transform: z ? [['translate3d', x, y, z]] : [['translate', x, y]] });
});
}
const result = super.animate(keyframes, options);
if (result) {
releaseAnimation(this, result);
animationMap.push(result);
}
if (Array.isArray(keyframes) && keyframes.length > 0) {
// 如果 keyframes 中仅存在 skippedAttrs 中的属性,则仅更新父元素属性(跳过子图形)
// if only skippedAttrs exist in keyframes, only update parent element attributes (skip child shapes)
const skippedAttrs = ['transform', 'transformOrigin', 'x', 'y', 'z', 'zIndex'];
if (Object.keys(keyframes[0]).some((attr) => !skippedAttrs.includes(attr))) {
Object.entries(this.shapeMap).forEach(([key, shape]) => {
// 如果存在方法名为 `get${key}Style` 的方法,则使用该方法获取样式,并自动为该图形实例创建动画
// if there is a method named `get${key}Style`, use this method to get style and automatically create animation for the shape instance
const methodName = `get${upperFirst(key)}Style` as keyof this;
const method = this[methodName];
if (isFunction(method)) {
const subKeyframes: Keyframe[] = keyframes.map((style) =>
method.call(this, { ...this.attributes, ...style }),
);
const result = shape.animate(preprocessKeyframes(subKeyframes), options);
if (result) {
releaseAnimation(shape, result);
animationMap.push(result);
}
}
});
const handleCompositeShapeAnimation = (shapeSet: Record<string, DisplayObject>, name: string) => {
if (!isEmpty(shapeSet)) {
const methodName = `get${upperFirst(name)}Style` as keyof this;
const method = this[methodName];
if (isFunction(method)) {
const itemsKeyframes = keyframes.map((style) => method.call(this, { ...this.attributes, ...style }));
Object.entries(itemsKeyframes[0]).map(([key]) => {
const subKeyframes = itemsKeyframes.map((styles) => styles[key]);
const shape = shapeSet[key];
if (shape) {
const result = shape.animate(preprocessKeyframes(subKeyframes), options);
if (result) {
releaseAnimation(shape, result);
animationMap.push(result);
}
}
});
}
}
};
this.compositeShapes.forEach(([key, prefix]) => {
const shapeSet = subObject(this.shapeMap, prefix);
handleCompositeShapeAnimation(shapeSet, key);
});
}
}
return createAnimationsProxy(animationMap);
}
public getShape<T extends DisplayObject>(name: string): T {
return this.shapeMap[name] as T;
}
private setVisibility() {
const { visibility } = this.attributes;
setVisibility(this, visibility);
}
public destroy(): void {
this.shapeMap = {};
this.animateMap = {};
super.destroy();
}
}
/**
* <zh/> 释放动画
*
* <en/> Release animation
* @param target - <zh/> 目标对象 | <en/> target object
* @param animation - <zh/> 动画实例 | <en/> animation instance
* @description see: https://github.com/antvis/G/issues/1731
*/
function releaseAnimation(target: DisplayObject, animation: IAnimation) {
animation?.finished.then(() => {
// @ts-expect-error private property
const index = target.activeAnimations.findIndex((_) => _ === animation);
// @ts-expect-error private property
if (index > -1) target.activeAnimations.splice(index, 1);
});
}
/**
* <zh/> 图形 upsert 方法生命周期钩子
*
* <en/> Shape upsert method lifecycle hooks
*/
export interface UpsertHooks {
/**
* <zh/> 图形创建前
*
* <en/> Before creating the shape
*/
beforeCreate?: () => void;
/**
* <zh/> 图形创建后
*
* <en/> After creating the shape
* @param instance - <zh/> 图形实例 | <en/> shape instance
*/
afterCreate?: (instance: DisplayObject) => void;
/**
* <zh/> 图形更新前
*
* <en/> Before updating the shape
* @param instance - <zh/> 图形实例 | <en/> shape instance
*/
beforeUpdate?: (instance: DisplayObject) => void;
/**
* <zh/> 图形更新后
*
* <en/> After updating the shape
* @param instance - <zh/> 图形实例 | <en/> shape instance
*/
afterUpdate?: (instance: DisplayObject) => void;
/**
* <zh/> 图形销毁前
*
* <en/> Before destroying the shape
* @param instance - <zh/> 图形实例 | <en/> shape instance
*/
beforeDestroy?: (instance: DisplayObject) => void;
/**
* <zh/> 图形销毁后
*
* <en/> After destroying the shape
* @param instance - <zh/> 图形实例 | <en/> shape instance
*/
afterDestroy?: (instance: DisplayObject) => void;
}
/**
* <zh/> 应用 transform
*
* <en/> Apply transform
* @param style - <zh/> 样式 | <en/> style
* @returns <zh/> 样式 | <en/> style
*/
function applyTransform(style?: BaseShapeStyleProps) {
if (!style) return {};
if ('x' in style || 'y' in style || 'z' in style) {
const { x = 0, y = 0, z, transform } = style as any;
const newTransform = replaceTranslateInTransform(x, y, z, transform);
if (newTransform) style.transform = newTransform;
}
return style;
}