cssAnimation.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import Transformable, { copyTransform } from '../core/Transformable';
  2. import Displayable from '../graphic/Displayable';
  3. import { SVGVNodeAttrs, BrushScope, createBrushScope} from './core';
  4. import Path from '../graphic/Path';
  5. import SVGPathRebuilder from './SVGPathRebuilder';
  6. import PathProxy from '../core/PathProxy';
  7. import { getPathPrecision, getSRTTransformString } from './helper';
  8. import { each, extend, filter, isNumber, isString, keys } from '../core/util';
  9. import Animator from '../animation/Animator';
  10. import CompoundPath from '../graphic/CompoundPath';
  11. import { AnimationEasing } from '../animation/easing';
  12. import { createCubicEasingFunc } from '../animation/cubicEasing';
  13. import { getClassId } from './cssClassId';
  14. export const EASING_MAP: Record<string, string> = {
  15. // From https://easings.net/
  16. cubicIn: '0.32,0,0.67,0',
  17. cubicOut: '0.33,1,0.68,1',
  18. cubicInOut: '0.65,0,0.35,1',
  19. quadraticIn: '0.11,0,0.5,0',
  20. quadraticOut: '0.5,1,0.89,1',
  21. quadraticInOut: '0.45,0,0.55,1',
  22. quarticIn: '0.5,0,0.75,0',
  23. quarticOut: '0.25,1,0.5,1',
  24. quarticInOut: '0.76,0,0.24,1',
  25. quinticIn: '0.64,0,0.78,0',
  26. quinticOut: '0.22,1,0.36,1',
  27. quinticInOut: '0.83,0,0.17,1',
  28. sinusoidalIn: '0.12,0,0.39,0',
  29. sinusoidalOut: '0.61,1,0.88,1',
  30. sinusoidalInOut: '0.37,0,0.63,1',
  31. exponentialIn: '0.7,0,0.84,0',
  32. exponentialOut: '0.16,1,0.3,1',
  33. exponentialInOut: '0.87,0,0.13,1',
  34. circularIn: '0.55,0,1,0.45',
  35. circularOut: '0,0.55,0.45,1',
  36. circularInOut: '0.85,0,0.15,1'
  37. // TODO elastic, bounce
  38. };
  39. const transformOriginKey = 'transform-origin';
  40. function buildPathString(el: Path, kfShape: Path['shape'], path: PathProxy) {
  41. const shape = extend({}, el.shape);
  42. extend(shape, kfShape);
  43. el.buildPath(path, shape);
  44. const svgPathBuilder = new SVGPathRebuilder();
  45. svgPathBuilder.reset(getPathPrecision(el));
  46. path.rebuildPath(svgPathBuilder, 1);
  47. svgPathBuilder.generateStr();
  48. // will add path("") when generated to css string in the final step.
  49. return svgPathBuilder.getStr();
  50. }
  51. function setTransformOrigin(target: Record<string, string>, transform: Transformable) {
  52. const {originX, originY} = transform;
  53. if (originX || originY) {
  54. target[transformOriginKey] = `${originX}px ${originY}px`;
  55. }
  56. }
  57. export const ANIMATE_STYLE_MAP: Record<string, string> = {
  58. fill: 'fill',
  59. opacity: 'opacity',
  60. lineWidth: 'stroke-width',
  61. lineDashOffset: 'stroke-dashoffset'
  62. // TODO shadow is not supported.
  63. };
  64. type CssKF = Record<string, any>;
  65. function addAnimation(cssAnim: Record<string, CssKF>, scope: BrushScope) {
  66. const animationName = scope.zrId + '-ani-' + scope.cssAnimIdx++;
  67. scope.cssAnims[animationName] = cssAnim;
  68. return animationName;
  69. }
  70. function createCompoundPathCSSAnimation(
  71. el: CompoundPath,
  72. attrs: SVGVNodeAttrs,
  73. scope: BrushScope
  74. ) {
  75. const paths = el.shape.paths;
  76. const composedAnim: Record<string, CssKF> = {};
  77. let cssAnimationCfg: string;
  78. let cssAnimationName: string;
  79. each(paths, path => {
  80. const subScope = createBrushScope(scope.zrId);
  81. subScope.animation = true;
  82. createCSSAnimation(path, {}, subScope, true);
  83. const cssAnims = subScope.cssAnims;
  84. const cssNodes = subScope.cssNodes;
  85. const animNames = keys(cssAnims);
  86. const len = animNames.length;
  87. if (!len) {
  88. return;
  89. }
  90. cssAnimationName = animNames[len - 1];
  91. // Only use last animation because they are conflicted.
  92. const lastAnim = cssAnims[cssAnimationName];
  93. // eslint-disable-next-line
  94. for (let percent in lastAnim) {
  95. const kf = lastAnim[percent];
  96. composedAnim[percent] = composedAnim[percent] || { d: '' };
  97. composedAnim[percent].d += kf.d || '';
  98. }
  99. // eslint-disable-next-line
  100. for (let className in cssNodes) {
  101. const val = cssNodes[className].animation;
  102. if (val.indexOf(cssAnimationName) >= 0) {
  103. // Only pick the animation configuration of last subpath.
  104. cssAnimationCfg = val;
  105. }
  106. }
  107. });
  108. if (!cssAnimationCfg) {
  109. return;
  110. }
  111. // Remove the attrs in the element because it will be set by animation.
  112. // Reduce the size.
  113. attrs.d = false;
  114. const animationName = addAnimation(composedAnim, scope);
  115. return cssAnimationCfg.replace(cssAnimationName, animationName);
  116. }
  117. function getEasingFunc(easing: AnimationEasing) {
  118. return isString(easing)
  119. ? EASING_MAP[easing]
  120. ? `cubic-bezier(${EASING_MAP[easing]})`
  121. : createCubicEasingFunc(easing) ? easing : ''
  122. : '';
  123. }
  124. export function createCSSAnimation(
  125. el: Displayable,
  126. attrs: SVGVNodeAttrs,
  127. scope: BrushScope,
  128. onlyShape?: boolean
  129. ) {
  130. const animators = el.animators;
  131. const len = animators.length;
  132. const cssAnimations: string[] = [];
  133. if (el instanceof CompoundPath) {
  134. const animationCfg = createCompoundPathCSSAnimation(el, attrs, scope);
  135. if (animationCfg) {
  136. cssAnimations.push(animationCfg);
  137. }
  138. else if (!len) {
  139. return;
  140. }
  141. }
  142. else if (!len) {
  143. return;
  144. }
  145. // Group animators by it's configuration
  146. const groupAnimators: Record<string, [string, Animator<any>[]]> = {};
  147. for (let i = 0; i < len; i++) {
  148. const animator = animators[i];
  149. const cfgArr: (string | number)[] = [animator.getMaxTime() / 1000 + 's'];
  150. const easing = getEasingFunc(animator.getClip().easing);
  151. const delay = animator.getDelay();
  152. if (easing) {
  153. cfgArr.push(easing);
  154. }
  155. else {
  156. cfgArr.push('linear');
  157. }
  158. if (delay) {
  159. cfgArr.push(delay / 1000 + 's');
  160. }
  161. if (animator.getLoop()) {
  162. cfgArr.push('infinite');
  163. }
  164. const cfg = cfgArr.join(' ');
  165. // TODO fill mode
  166. groupAnimators[cfg] = groupAnimators[cfg] || [cfg, [] as Animator<any>[]];
  167. groupAnimators[cfg][1].push(animator);
  168. }
  169. function createSingleCSSAnimation(groupAnimator: [string, Animator<any>[]]) {
  170. const animators = groupAnimator[1];
  171. const len = animators.length;
  172. const transformKfs: Record<string, CssKF> = {};
  173. const shapeKfs: Record<string, CssKF> = {};
  174. const finalKfs: Record<string, CssKF> = {};
  175. const animationTimingFunctionAttrName = 'animation-timing-function';
  176. function saveAnimatorTrackToCssKfs(
  177. animator: Animator<any>,
  178. cssKfs: Record<string, CssKF>,
  179. toCssAttrName?: (propName: string) => string
  180. ) {
  181. const tracks = animator.getTracks();
  182. const maxTime = animator.getMaxTime();
  183. for (let k = 0; k < tracks.length; k++) {
  184. const track = tracks[k];
  185. if (track.needsAnimate()) {
  186. const kfs = track.keyframes;
  187. let attrName = track.propName;
  188. toCssAttrName && (attrName = toCssAttrName(attrName));
  189. if (attrName) {
  190. for (let i = 0; i < kfs.length; i++) {
  191. const kf = kfs[i];
  192. const percent = Math.round(kf.time / maxTime * 100) + '%';
  193. const kfEasing = getEasingFunc(kf.easing);
  194. const rawValue = kf.rawValue;
  195. // TODO gradient
  196. if (isString(rawValue) || isNumber(rawValue)) {
  197. cssKfs[percent] = cssKfs[percent] || {};
  198. cssKfs[percent][attrName] = kf.rawValue;
  199. if (kfEasing) {
  200. // TODO. If different property have different easings.
  201. cssKfs[percent][animationTimingFunctionAttrName] = kfEasing;
  202. }
  203. }
  204. }
  205. }
  206. }
  207. }
  208. }
  209. // Find all transform animations.
  210. // TODO origin, parent
  211. for (let i = 0; i < len; i++) {
  212. const animator = animators[i];
  213. const targetProp = animator.targetName;
  214. if (!targetProp) {
  215. !onlyShape && saveAnimatorTrackToCssKfs(animator, transformKfs);
  216. }
  217. else if (targetProp === 'shape') {
  218. saveAnimatorTrackToCssKfs(animator, shapeKfs);
  219. }
  220. }
  221. // eslint-disable-next-line
  222. for (let percent in transformKfs) {
  223. const transform = {} as Transformable;
  224. copyTransform(transform, el);
  225. extend(transform, transformKfs[percent]);
  226. const str = getSRTTransformString(transform);
  227. const timingFunction = transformKfs[percent][animationTimingFunctionAttrName];
  228. finalKfs[percent] = str ? {
  229. transform: str
  230. } : {};
  231. // TODO set transform origin in element?
  232. setTransformOrigin(finalKfs[percent], transform);
  233. // Save timing function
  234. if (timingFunction) {
  235. finalKfs[percent][animationTimingFunctionAttrName] = timingFunction;
  236. }
  237. };
  238. let path: PathProxy;
  239. let canAnimateShape = true;
  240. // eslint-disable-next-line
  241. for (let percent in shapeKfs) {
  242. finalKfs[percent] = finalKfs[percent] || {};
  243. const isFirst = !path;
  244. const timingFunction = shapeKfs[percent][animationTimingFunctionAttrName];
  245. if (isFirst) {
  246. path = new PathProxy();
  247. }
  248. let len = path.len();
  249. path.reset();
  250. finalKfs[percent].d = buildPathString(el as Path, shapeKfs[percent], path);
  251. let newLen = path.len();
  252. // Path data don't match.
  253. if (!isFirst && len !== newLen) {
  254. canAnimateShape = false;
  255. break;
  256. }
  257. // Save timing function
  258. if (timingFunction) {
  259. finalKfs[percent][animationTimingFunctionAttrName] = timingFunction;
  260. }
  261. };
  262. if (!canAnimateShape) {
  263. // eslint-disable-next-line
  264. for (let percent in finalKfs) {
  265. delete finalKfs[percent].d;
  266. }
  267. }
  268. if (!onlyShape) {
  269. for (let i = 0; i < len; i++) {
  270. const animator = animators[i];
  271. const targetProp = animator.targetName;
  272. if (targetProp === 'style') {
  273. saveAnimatorTrackToCssKfs(
  274. animator, finalKfs, (propName) => ANIMATE_STYLE_MAP[propName]
  275. );
  276. }
  277. }
  278. }
  279. const percents = keys(finalKfs);
  280. // Set transform origin in attribute to reduce the size.
  281. let allTransformOriginSame = true;
  282. let transformOrigin;
  283. for (let i = 1; i < percents.length; i++) {
  284. const p0 = percents[i - 1];
  285. const p1 = percents[i];
  286. if (finalKfs[p0][transformOriginKey] !== finalKfs[p1][transformOriginKey]) {
  287. allTransformOriginSame = false;
  288. break;
  289. }
  290. transformOrigin = finalKfs[p0][transformOriginKey];
  291. }
  292. if (allTransformOriginSame && transformOrigin) {
  293. for (const percent in finalKfs) {
  294. if (finalKfs[percent][transformOriginKey]) {
  295. delete finalKfs[percent][transformOriginKey];
  296. }
  297. }
  298. attrs[transformOriginKey] = transformOrigin;
  299. }
  300. if (filter(
  301. percents, (percent) => keys(finalKfs[percent]).length > 0
  302. ).length) {
  303. const animationName = addAnimation(finalKfs, scope);
  304. // eslint-disable-next-line
  305. // for (const attrName in finalKfs[percents[0]]) {
  306. // // Remove the attrs in the element because it will be set by animation.
  307. // // Reduce the size.
  308. // attrs[attrName] = false;
  309. // }
  310. // animationName {duration easing delay loop} fillMode
  311. return `${animationName} ${groupAnimator[0]} both`;
  312. }
  313. }
  314. // eslint-disable-next-line
  315. for (let key in groupAnimators) {
  316. const animationCfg = createSingleCSSAnimation(groupAnimators[key]);
  317. if (animationCfg) {
  318. cssAnimations.push(animationCfg);
  319. }
  320. }
  321. if (cssAnimations.length) {
  322. const className = scope.zrId + '-cls-' + getClassId();
  323. scope.cssNodes['.' + className] = {
  324. animation: cssAnimations.join(',')
  325. };
  326. // TODO exists class?
  327. attrs.class = className;
  328. }
  329. }