UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

291 lines (265 loc) 7.83 kB
import { convertToPath, DisplayObject, IAnimation as GAnimation, Path, Shape, } from '@antv/g'; import { AnimationComponent as AC } from '../runtime'; import { copyAttributes } from '../utils/helper'; import { Animation } from './types'; import { attributeKeys, attributeOf, GEOMETRY_ATTRIBUTES } from './utils'; export type MorphingOptions = Animation & { split: 'pack' | SplitFunction }; type BBox = [number, number, number, number]; type SplitFunction = (shape: DisplayObject, count: number) => string[]; function localBBoxOf(shape: DisplayObject): BBox { const { min, max } = shape.getLocalBounds(); const [x0, y0] = min; const [x1, y1] = max; const height = y1 - y0; const width = x1 - x0; return [x0, y0, width, height]; } function d(bbox: BBox): string { const [x, y, width, height] = bbox; return ` M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z `; } function pack(shape: DisplayObject, count: number): string[] { const [x0, y0, width, height] = localBBoxOf(shape); const aspect = height / width; const col = Math.ceil(Math.sqrt(count / aspect)); const row = Math.ceil(count / col); const B = []; const h = height / row; let j = 0; let n = count; while (n > 0) { const c = Math.min(n, col); const w = width / c; for (let i = 0; i < c; i++) { const x = x0 + i * w; const y = y0 + j * h; B.push(d([x, y, w, h])); } n -= c; j += 1; } return B; } function normalizeSplit( split: MorphingOptions['split'] = 'pack', ): SplitFunction { if (typeof split == 'function') return split; return pack; } /** * Use attributes relative to geometry to do shape to shape animation. * * For example, the x, y, width, height of `Rect`, the cx, cy, r of `Circle`. * And for `Group`, it will use the bbox of the group. */ function shapeToShape( from: DisplayObject, to: DisplayObject, timeEffect: Record<string, any>, ): GAnimation { let { transform: fromTransform } = from.style; const { transform: toTransform } = to.style; // Replace first to get right bbox after mounting. replaceChild(to, from); let keys = attributeKeys; if (from.nodeName === Shape.GROUP) { // Apply translate and scale transform. const [x0, y0, w0, h0] = localBBoxOf(from); const [x1, y1, w1, h1] = localBBoxOf(to); const dx = x0 - x1; const dy = y0 - y1; const sx = w0 / w1; const sy = h0 / h1; fromTransform = `translate(${dx}, ${dy}) scale(${sx}, ${sy})`; } else { keys = keys.concat(GEOMETRY_ATTRIBUTES[from.nodeName] || []); } const keyframes = [ { transform: fromTransform ?? 'none', ...attributeOf(from, keys, true), }, { transform: toTransform ?? 'none', ...attributeOf(to, keys, true), }, ]; const animation = to.animate(keyframes, timeEffect); return animation; } /** * Replace object and copy className and __data__ */ function replaceChild(newChild: DisplayObject, oldChild: DisplayObject) { newChild['__data__'] = oldChild['__data__']; newChild.className = oldChild.className; // @ts-ignore newChild.markType = oldChild.markType; oldChild.parentNode.replaceChild(newChild, oldChild); } /** * Replace element with a path shape. */ function maybePath(node: DisplayObject, d: string): DisplayObject { const { nodeName } = node; if (nodeName === 'path') return node; const path = new Path({ style: { ...attributeOf(node, attributeKeys), d, }, }); replaceChild(path, node); return path; } function hasUniqueString(search: string, pattern: string): boolean { const first = search.indexOf(pattern); const last = search.lastIndexOf(pattern); return first === last; } // Path definition with multiple m and M command has sub path. // eg. 'M10,10...M20,20', 'm10,10...m20,20' function hasSubPath(path: string): boolean { return !hasUniqueString(path, 'm') || !hasUniqueString(path, 'M'); } function shape2path(shape: DisplayObject): string { const path = convertToPath(shape); if (!path) return; // Path definition with sub path can't do path morphing animation, // so skip this kind of path. if (hasSubPath(path)) return; return path; } function oneToOne( shape: DisplayObject, from: DisplayObject, to: DisplayObject, timeEffect: Record<string, any>, ) { // If the nodeTypes of from and to are equal, // or non of them can convert to path, // the apply shape to shape animation. const { nodeName: fromName } = from; const { nodeName: toName } = to; const fromPath = shape2path(from); const toPath = shape2path(to); const isSameNodes = fromName === toName && fromName !== 'path'; const hasNonPathNode = fromPath === undefined || toPath === undefined; if (isSameNodes || hasNonPathNode) return shapeToShape(from, to, timeEffect); const pathShape = maybePath(shape, fromPath); // Convert Path will take transform, anchor, etc into account, // so there is no need to specify these attributes in keyframes. const keyframes: Keyframe[] = [ { ...attributeOf(from, attributeKeys), }, { ...attributeOf(to, attributeKeys), }, ]; if (fromPath !== toPath) { keyframes[0].d = fromPath; keyframes[1].d = toPath; const animation = pathShape.animate(keyframes, timeEffect); animation.onfinish = () => { // Should keep the original path definition. const d = pathShape.style.d; copyAttributes(pathShape, to); pathShape.style.d = d; pathShape.style.transform = 'none'; }; // Remove transform because it already applied in path // converted by convertToPath. pathShape.style.transform = 'none'; return animation; } // No need to apply animation since fromPath equals toPath. return null; } function oneToMultiple( from: DisplayObject, to: DisplayObject[], timeEffect: Record<string, any>, split: SplitFunction, ) { // Hide the shape to be split before being removing. from.style.visibility = 'hidden'; const D = split(from, to.length); return to.map((shape, i) => { const path = new Path({ style: { d: D[i], ...attributeOf(from, attributeKeys), }, }); return oneToOne(shape, path, shape, timeEffect); }); } function multipleToOne( from: DisplayObject[], to: DisplayObject, timeEffect: Record<string, any>, split: SplitFunction, ) { const D = split(to, from.length); const { fillOpacity = 1, strokeOpacity = 1, opacity = 1 } = to.style; const keyframes = [ { fillOpacity: 0, strokeOpacity: 0, opacity: 0 }, { fillOpacity: 0, strokeOpacity: 0, opacity: 0, offset: 0.99 }, { fillOpacity, strokeOpacity, opacity, }, ]; const animation = to.animate(keyframes, timeEffect); const animations = from.map((shape, i) => { const path = new Path({ style: { d: D[i], fill: to.style.fill, }, }); return oneToOne(shape, shape, path, timeEffect); }); return [...animations, animation]; } /** * Morphing animations. * @todo Support more split function. */ export const Morphing: AC<MorphingOptions> = (options) => { return (from, to, defaults) => { const split = normalizeSplit(options.split); const timeEffect = { ...defaults, ...options }; const { length: fl } = from; const { length: tl } = to; if ((fl === 1 && tl === 1) || (fl > 1 && tl > 1)) { const [f] = from; const [t] = to; return oneToOne(f, f, t, timeEffect); } if (fl === 1 && tl > 1) { const [f] = from; return oneToMultiple(f, to, timeEffect, split); } if (fl > 1 && tl === 1) { const [t] = to; return multipleToOne(from, t, timeEffect, split); } return null; }; }; Morphing.props = {};