UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

366 lines (329 loc) 12.9 kB
import Transformable, { copyTransform } from '../core/Transformable'; import Displayable from '../graphic/Displayable'; import { SVGVNodeAttrs, BrushScope, createBrushScope} from './core'; import Path from '../graphic/Path'; import SVGPathRebuilder from './SVGPathRebuilder'; import PathProxy from '../core/PathProxy'; import { getPathPrecision, getSRTTransformString } from './helper'; import { each, extend, filter, isNumber, isString, keys } from '../core/util'; import Animator from '../animation/Animator'; import CompoundPath from '../graphic/CompoundPath'; import { AnimationEasing } from '../animation/easing'; import { createCubicEasingFunc } from '../animation/cubicEasing'; import { getClassId } from './cssClassId'; export const EASING_MAP: Record<string, string> = { // From https://easings.net/ cubicIn: '0.32,0,0.67,0', cubicOut: '0.33,1,0.68,1', cubicInOut: '0.65,0,0.35,1', quadraticIn: '0.11,0,0.5,0', quadraticOut: '0.5,1,0.89,1', quadraticInOut: '0.45,0,0.55,1', quarticIn: '0.5,0,0.75,0', quarticOut: '0.25,1,0.5,1', quarticInOut: '0.76,0,0.24,1', quinticIn: '0.64,0,0.78,0', quinticOut: '0.22,1,0.36,1', quinticInOut: '0.83,0,0.17,1', sinusoidalIn: '0.12,0,0.39,0', sinusoidalOut: '0.61,1,0.88,1', sinusoidalInOut: '0.37,0,0.63,1', exponentialIn: '0.7,0,0.84,0', exponentialOut: '0.16,1,0.3,1', exponentialInOut: '0.87,0,0.13,1', circularIn: '0.55,0,1,0.45', circularOut: '0,0.55,0.45,1', circularInOut: '0.85,0,0.15,1' // TODO elastic, bounce }; const transformOriginKey = 'transform-origin'; function buildPathString(el: Path, kfShape: Path['shape'], path: PathProxy) { const shape = extend({}, el.shape); extend(shape, kfShape); el.buildPath(path, shape); const svgPathBuilder = new SVGPathRebuilder(); svgPathBuilder.reset(getPathPrecision(el)); path.rebuildPath(svgPathBuilder, 1); svgPathBuilder.generateStr(); // will add path("") when generated to css string in the final step. return svgPathBuilder.getStr(); } function setTransformOrigin(target: Record<string, string>, transform: Transformable) { const {originX, originY} = transform; if (originX || originY) { target[transformOriginKey] = `${originX}px ${originY}px`; } } export const ANIMATE_STYLE_MAP: Record<string, string> = { fill: 'fill', opacity: 'opacity', lineWidth: 'stroke-width', lineDashOffset: 'stroke-dashoffset' // TODO shadow is not supported. }; type CssKF = Record<string, any>; function addAnimation(cssAnim: Record<string, CssKF>, scope: BrushScope) { const animationName = scope.zrId + '-ani-' + scope.cssAnimIdx++; scope.cssAnims[animationName] = cssAnim; return animationName; } function createCompoundPathCSSAnimation( el: CompoundPath, attrs: SVGVNodeAttrs, scope: BrushScope ) { const paths = el.shape.paths; const composedAnim: Record<string, CssKF> = {}; let cssAnimationCfg: string; let cssAnimationName: string; each(paths, path => { const subScope = createBrushScope(scope.zrId); subScope.animation = true; createCSSAnimation(path, {}, subScope, true); const cssAnims = subScope.cssAnims; const cssNodes = subScope.cssNodes; const animNames = keys(cssAnims); const len = animNames.length; if (!len) { return; } cssAnimationName = animNames[len - 1]; // Only use last animation because they are conflicted. const lastAnim = cssAnims[cssAnimationName]; // eslint-disable-next-line for (let percent in lastAnim) { const kf = lastAnim[percent]; composedAnim[percent] = composedAnim[percent] || { d: '' }; composedAnim[percent].d += kf.d || ''; } // eslint-disable-next-line for (let className in cssNodes) { const val = cssNodes[className].animation; if (val.indexOf(cssAnimationName) >= 0) { // Only pick the animation configuration of last subpath. cssAnimationCfg = val; } } }); if (!cssAnimationCfg) { return; } // Remove the attrs in the element because it will be set by animation. // Reduce the size. attrs.d = false; const animationName = addAnimation(composedAnim, scope); return cssAnimationCfg.replace(cssAnimationName, animationName); } function getEasingFunc(easing: AnimationEasing) { return isString(easing) ? EASING_MAP[easing] ? `cubic-bezier(${EASING_MAP[easing]})` : createCubicEasingFunc(easing) ? easing : '' : ''; } export function createCSSAnimation( el: Displayable, attrs: SVGVNodeAttrs, scope: BrushScope, onlyShape?: boolean ) { const animators = el.animators; const len = animators.length; const cssAnimations: string[] = []; if (el instanceof CompoundPath) { const animationCfg = createCompoundPathCSSAnimation(el, attrs, scope); if (animationCfg) { cssAnimations.push(animationCfg); } else if (!len) { return; } } else if (!len) { return; } // Group animators by it's configuration const groupAnimators: Record<string, [string, Animator<any>[]]> = {}; for (let i = 0; i < len; i++) { const animator = animators[i]; const cfgArr: (string | number)[] = [animator.getMaxTime() / 1000 + 's']; const easing = getEasingFunc(animator.getClip().easing); const delay = animator.getDelay(); if (easing) { cfgArr.push(easing); } else { cfgArr.push('linear'); } if (delay) { cfgArr.push(delay / 1000 + 's'); } if (animator.getLoop()) { cfgArr.push('infinite'); } const cfg = cfgArr.join(' '); // TODO fill mode groupAnimators[cfg] = groupAnimators[cfg] || [cfg, [] as Animator<any>[]]; groupAnimators[cfg][1].push(animator); } function createSingleCSSAnimation(groupAnimator: [string, Animator<any>[]]) { const animators = groupAnimator[1]; const len = animators.length; const transformKfs: Record<string, CssKF> = {}; const shapeKfs: Record<string, CssKF> = {}; const finalKfs: Record<string, CssKF> = {}; const animationTimingFunctionAttrName = 'animation-timing-function'; function saveAnimatorTrackToCssKfs( animator: Animator<any>, cssKfs: Record<string, CssKF>, toCssAttrName?: (propName: string) => string ) { const tracks = animator.getTracks(); const maxTime = animator.getMaxTime(); for (let k = 0; k < tracks.length; k++) { const track = tracks[k]; if (track.needsAnimate()) { const kfs = track.keyframes; let attrName = track.propName; toCssAttrName && (attrName = toCssAttrName(attrName)); if (attrName) { for (let i = 0; i < kfs.length; i++) { const kf = kfs[i]; const percent = Math.round(kf.time / maxTime * 100) + '%'; const kfEasing = getEasingFunc(kf.easing); const rawValue = kf.rawValue; // TODO gradient if (isString(rawValue) || isNumber(rawValue)) { cssKfs[percent] = cssKfs[percent] || {}; cssKfs[percent][attrName] = kf.rawValue; if (kfEasing) { // TODO. If different property have different easings. cssKfs[percent][animationTimingFunctionAttrName] = kfEasing; } } } } } } } // Find all transform animations. // TODO origin, parent for (let i = 0; i < len; i++) { const animator = animators[i]; const targetProp = animator.targetName; if (!targetProp) { !onlyShape && saveAnimatorTrackToCssKfs(animator, transformKfs); } else if (targetProp === 'shape') { saveAnimatorTrackToCssKfs(animator, shapeKfs); } } // eslint-disable-next-line for (let percent in transformKfs) { const transform = {} as Transformable; copyTransform(transform, el); extend(transform, transformKfs[percent]); const str = getSRTTransformString(transform); const timingFunction = transformKfs[percent][animationTimingFunctionAttrName]; finalKfs[percent] = str ? { transform: str } : {}; // TODO set transform origin in element? setTransformOrigin(finalKfs[percent], transform); // Save timing function if (timingFunction) { finalKfs[percent][animationTimingFunctionAttrName] = timingFunction; } }; let path: PathProxy; let canAnimateShape = true; // eslint-disable-next-line for (let percent in shapeKfs) { finalKfs[percent] = finalKfs[percent] || {}; const isFirst = !path; const timingFunction = shapeKfs[percent][animationTimingFunctionAttrName]; if (isFirst) { path = new PathProxy(); } let len = path.len(); path.reset(); finalKfs[percent].d = buildPathString(el as Path, shapeKfs[percent], path); let newLen = path.len(); // Path data don't match. if (!isFirst && len !== newLen) { canAnimateShape = false; break; } // Save timing function if (timingFunction) { finalKfs[percent][animationTimingFunctionAttrName] = timingFunction; } }; if (!canAnimateShape) { // eslint-disable-next-line for (let percent in finalKfs) { delete finalKfs[percent].d; } } if (!onlyShape) { for (let i = 0; i < len; i++) { const animator = animators[i]; const targetProp = animator.targetName; if (targetProp === 'style') { saveAnimatorTrackToCssKfs( animator, finalKfs, (propName) => ANIMATE_STYLE_MAP[propName] ); } } } const percents = keys(finalKfs); // Set transform origin in attribute to reduce the size. let allTransformOriginSame = true; let transformOrigin; for (let i = 1; i < percents.length; i++) { const p0 = percents[i - 1]; const p1 = percents[i]; if (finalKfs[p0][transformOriginKey] !== finalKfs[p1][transformOriginKey]) { allTransformOriginSame = false; break; } transformOrigin = finalKfs[p0][transformOriginKey]; } if (allTransformOriginSame && transformOrigin) { for (const percent in finalKfs) { if (finalKfs[percent][transformOriginKey]) { delete finalKfs[percent][transformOriginKey]; } } attrs[transformOriginKey] = transformOrigin; } if (filter( percents, (percent) => keys(finalKfs[percent]).length > 0 ).length) { const animationName = addAnimation(finalKfs, scope); // eslint-disable-next-line // for (const attrName in finalKfs[percents[0]]) { // // Remove the attrs in the element because it will be set by animation. // // Reduce the size. // attrs[attrName] = false; // } // animationName {duration easing delay loop} fillMode return `${animationName} ${groupAnimator[0]} both`; } } // eslint-disable-next-line for (let key in groupAnimators) { const animationCfg = createSingleCSSAnimation(groupAnimators[key]); if (animationCfg) { cssAnimations.push(animationCfg); } } if (cssAnimations.length) { const className = scope.zrId + '-cls-' + getClassId(); scope.cssNodes['.' + className] = { animation: cssAnimations.join(',') }; // TODO exists class? attrs.class = className; } }