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