UNPKG

@antv/g6

Version:

A Graph Visualization Framework in JavaScript

336 lines (304 loc) 11.4 kB
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; }