8486205473a3929c0b984b45097bdf8d7d7887b869add7e2c161f1f7b638ea3e1d3e0df32d3081facf4da88ed3f35eb177342503cd86518c11fd876d8319ec 13 KB

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