| 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; | 
|     } | 
| } |