Animator.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. var Clip = require("./Clip");
  2. var color = require("../tool/color");
  3. var _util = require("../core/util");
  4. var isArrayLike = _util.isArrayLike;
  5. /**
  6. * @module echarts/animation/Animator
  7. */
  8. var arraySlice = Array.prototype.slice;
  9. function defaultGetter(target, key) {
  10. return target[key];
  11. }
  12. function defaultSetter(target, key, value) {
  13. target[key] = value;
  14. }
  15. /**
  16. * @param {number} p0
  17. * @param {number} p1
  18. * @param {number} percent
  19. * @return {number}
  20. */
  21. function interpolateNumber(p0, p1, percent) {
  22. return (p1 - p0) * percent + p0;
  23. }
  24. /**
  25. * @param {string} p0
  26. * @param {string} p1
  27. * @param {number} percent
  28. * @return {string}
  29. */
  30. function interpolateString(p0, p1, percent) {
  31. return percent > 0.5 ? p1 : p0;
  32. }
  33. /**
  34. * @param {Array} p0
  35. * @param {Array} p1
  36. * @param {number} percent
  37. * @param {Array} out
  38. * @param {number} arrDim
  39. */
  40. function interpolateArray(p0, p1, percent, out, arrDim) {
  41. var len = p0.length;
  42. if (arrDim === 1) {
  43. for (var i = 0; i < len; i++) {
  44. out[i] = interpolateNumber(p0[i], p1[i], percent);
  45. }
  46. } else {
  47. var len2 = len && p0[0].length;
  48. for (var i = 0; i < len; i++) {
  49. for (var j = 0; j < len2; j++) {
  50. out[i][j] = interpolateNumber(p0[i][j], p1[i][j], percent);
  51. }
  52. }
  53. }
  54. } // arr0 is source array, arr1 is target array.
  55. // Do some preprocess to avoid error happened when interpolating from arr0 to arr1
  56. function fillArr(arr0, arr1, arrDim) {
  57. var arr0Len = arr0.length;
  58. var arr1Len = arr1.length;
  59. if (arr0Len !== arr1Len) {
  60. // FIXME Not work for TypedArray
  61. var isPreviousLarger = arr0Len > arr1Len;
  62. if (isPreviousLarger) {
  63. // Cut the previous
  64. arr0.length = arr1Len;
  65. } else {
  66. // Fill the previous
  67. for (var i = arr0Len; i < arr1Len; i++) {
  68. arr0.push(arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i]));
  69. }
  70. }
  71. } // Handling NaN value
  72. var len2 = arr0[0] && arr0[0].length;
  73. for (var i = 0; i < arr0.length; i++) {
  74. if (arrDim === 1) {
  75. if (isNaN(arr0[i])) {
  76. arr0[i] = arr1[i];
  77. }
  78. } else {
  79. for (var j = 0; j < len2; j++) {
  80. if (isNaN(arr0[i][j])) {
  81. arr0[i][j] = arr1[i][j];
  82. }
  83. }
  84. }
  85. }
  86. }
  87. /**
  88. * @param {Array} arr0
  89. * @param {Array} arr1
  90. * @param {number} arrDim
  91. * @return {boolean}
  92. */
  93. function isArraySame(arr0, arr1, arrDim) {
  94. if (arr0 === arr1) {
  95. return true;
  96. }
  97. var len = arr0.length;
  98. if (len !== arr1.length) {
  99. return false;
  100. }
  101. if (arrDim === 1) {
  102. for (var i = 0; i < len; i++) {
  103. if (arr0[i] !== arr1[i]) {
  104. return false;
  105. }
  106. }
  107. } else {
  108. var len2 = arr0[0].length;
  109. for (var i = 0; i < len; i++) {
  110. for (var j = 0; j < len2; j++) {
  111. if (arr0[i][j] !== arr1[i][j]) {
  112. return false;
  113. }
  114. }
  115. }
  116. }
  117. return true;
  118. }
  119. /**
  120. * Catmull Rom interpolate array
  121. * @param {Array} p0
  122. * @param {Array} p1
  123. * @param {Array} p2
  124. * @param {Array} p3
  125. * @param {number} t
  126. * @param {number} t2
  127. * @param {number} t3
  128. * @param {Array} out
  129. * @param {number} arrDim
  130. */
  131. function catmullRomInterpolateArray(p0, p1, p2, p3, t, t2, t3, out, arrDim) {
  132. var len = p0.length;
  133. if (arrDim === 1) {
  134. for (var i = 0; i < len; i++) {
  135. out[i] = catmullRomInterpolate(p0[i], p1[i], p2[i], p3[i], t, t2, t3);
  136. }
  137. } else {
  138. var len2 = p0[0].length;
  139. for (var i = 0; i < len; i++) {
  140. for (var j = 0; j < len2; j++) {
  141. out[i][j] = catmullRomInterpolate(p0[i][j], p1[i][j], p2[i][j], p3[i][j], t, t2, t3);
  142. }
  143. }
  144. }
  145. }
  146. /**
  147. * Catmull Rom interpolate number
  148. * @param {number} p0
  149. * @param {number} p1
  150. * @param {number} p2
  151. * @param {number} p3
  152. * @param {number} t
  153. * @param {number} t2
  154. * @param {number} t3
  155. * @return {number}
  156. */
  157. function catmullRomInterpolate(p0, p1, p2, p3, t, t2, t3) {
  158. var v0 = (p2 - p0) * 0.5;
  159. var v1 = (p3 - p1) * 0.5;
  160. return (2 * (p1 - p2) + v0 + v1) * t3 + (-3 * (p1 - p2) - 2 * v0 - v1) * t2 + v0 * t + p1;
  161. }
  162. function cloneValue(value) {
  163. if (isArrayLike(value)) {
  164. var len = value.length;
  165. if (isArrayLike(value[0])) {
  166. var ret = [];
  167. for (var i = 0; i < len; i++) {
  168. ret.push(arraySlice.call(value[i]));
  169. }
  170. return ret;
  171. }
  172. return arraySlice.call(value);
  173. }
  174. return value;
  175. }
  176. function rgba2String(rgba) {
  177. rgba[0] = Math.floor(rgba[0]);
  178. rgba[1] = Math.floor(rgba[1]);
  179. rgba[2] = Math.floor(rgba[2]);
  180. return 'rgba(' + rgba.join(',') + ')';
  181. }
  182. function getArrayDim(keyframes) {
  183. var lastValue = keyframes[keyframes.length - 1].value;
  184. return isArrayLike(lastValue && lastValue[0]) ? 2 : 1;
  185. }
  186. function createTrackClip(animator, easing, oneTrackDone, keyframes, propName, forceAnimate) {
  187. var getter = animator._getter;
  188. var setter = animator._setter;
  189. var useSpline = easing === 'spline';
  190. var trackLen = keyframes.length;
  191. if (!trackLen) {
  192. return;
  193. } // Guess data type
  194. var firstVal = keyframes[0].value;
  195. var isValueArray = isArrayLike(firstVal);
  196. var isValueColor = false;
  197. var isValueString = false; // For vertices morphing
  198. var arrDim = isValueArray ? getArrayDim(keyframes) : 0;
  199. var trackMaxTime; // Sort keyframe as ascending
  200. keyframes.sort(function (a, b) {
  201. return a.time - b.time;
  202. });
  203. trackMaxTime = keyframes[trackLen - 1].time; // Percents of each keyframe
  204. var kfPercents = []; // Value of each keyframe
  205. var kfValues = [];
  206. var prevValue = keyframes[0].value;
  207. var isAllValueEqual = true;
  208. for (var i = 0; i < trackLen; i++) {
  209. kfPercents.push(keyframes[i].time / trackMaxTime); // Assume value is a color when it is a string
  210. var value = keyframes[i].value; // Check if value is equal, deep check if value is array
  211. if (!(isValueArray && isArraySame(value, prevValue, arrDim) || !isValueArray && value === prevValue)) {
  212. isAllValueEqual = false;
  213. }
  214. prevValue = value; // Try converting a string to a color array
  215. if (typeof value === 'string') {
  216. var colorArray = color.parse(value);
  217. if (colorArray) {
  218. value = colorArray;
  219. isValueColor = true;
  220. } else {
  221. isValueString = true;
  222. }
  223. }
  224. kfValues.push(value);
  225. }
  226. if (!forceAnimate && isAllValueEqual) {
  227. return;
  228. }
  229. var lastValue = kfValues[trackLen - 1]; // Polyfill array and NaN value
  230. for (var i = 0; i < trackLen - 1; i++) {
  231. if (isValueArray) {
  232. fillArr(kfValues[i], lastValue, arrDim);
  233. } else {
  234. if (isNaN(kfValues[i]) && !isNaN(lastValue) && !isValueString && !isValueColor) {
  235. kfValues[i] = lastValue;
  236. }
  237. }
  238. }
  239. isValueArray && fillArr(getter(animator._target, propName), lastValue, arrDim); // Cache the key of last frame to speed up when
  240. // animation playback is sequency
  241. var lastFrame = 0;
  242. var lastFramePercent = 0;
  243. var start;
  244. var w;
  245. var p0;
  246. var p1;
  247. var p2;
  248. var p3;
  249. if (isValueColor) {
  250. var rgba = [0, 0, 0, 0];
  251. }
  252. var onframe = function (target, percent) {
  253. // Find the range keyframes
  254. // kf1-----kf2---------current--------kf3
  255. // find kf2 and kf3 and do interpolation
  256. var frame; // In the easing function like elasticOut, percent may less than 0
  257. if (percent < 0) {
  258. frame = 0;
  259. } else if (percent < lastFramePercent) {
  260. // Start from next key
  261. // PENDING start from lastFrame ?
  262. start = Math.min(lastFrame + 1, trackLen - 1);
  263. for (frame = start; frame >= 0; frame--) {
  264. if (kfPercents[frame] <= percent) {
  265. break;
  266. }
  267. } // PENDING really need to do this ?
  268. frame = Math.min(frame, trackLen - 2);
  269. } else {
  270. for (frame = lastFrame; frame < trackLen; frame++) {
  271. if (kfPercents[frame] > percent) {
  272. break;
  273. }
  274. }
  275. frame = Math.min(frame - 1, trackLen - 2);
  276. }
  277. lastFrame = frame;
  278. lastFramePercent = percent;
  279. var range = kfPercents[frame + 1] - kfPercents[frame];
  280. if (range === 0) {
  281. return;
  282. } else {
  283. w = (percent - kfPercents[frame]) / range;
  284. }
  285. if (useSpline) {
  286. p1 = kfValues[frame];
  287. p0 = kfValues[frame === 0 ? frame : frame - 1];
  288. p2 = kfValues[frame > trackLen - 2 ? trackLen - 1 : frame + 1];
  289. p3 = kfValues[frame > trackLen - 3 ? trackLen - 1 : frame + 2];
  290. if (isValueArray) {
  291. catmullRomInterpolateArray(p0, p1, p2, p3, w, w * w, w * w * w, getter(target, propName), arrDim);
  292. } else {
  293. var value;
  294. if (isValueColor) {
  295. value = catmullRomInterpolateArray(p0, p1, p2, p3, w, w * w, w * w * w, rgba, 1);
  296. value = rgba2String(rgba);
  297. } else if (isValueString) {
  298. // String is step(0.5)
  299. return interpolateString(p1, p2, w);
  300. } else {
  301. value = catmullRomInterpolate(p0, p1, p2, p3, w, w * w, w * w * w);
  302. }
  303. setter(target, propName, value);
  304. }
  305. } else {
  306. if (isValueArray) {
  307. interpolateArray(kfValues[frame], kfValues[frame + 1], w, getter(target, propName), arrDim);
  308. } else {
  309. var value;
  310. if (isValueColor) {
  311. interpolateArray(kfValues[frame], kfValues[frame + 1], w, rgba, 1);
  312. value = rgba2String(rgba);
  313. } else if (isValueString) {
  314. // String is step(0.5)
  315. return interpolateString(kfValues[frame], kfValues[frame + 1], w);
  316. } else {
  317. value = interpolateNumber(kfValues[frame], kfValues[frame + 1], w);
  318. }
  319. setter(target, propName, value);
  320. }
  321. }
  322. };
  323. var clip = new Clip({
  324. target: animator._target,
  325. life: trackMaxTime,
  326. loop: animator._loop,
  327. delay: animator._delay,
  328. onframe: onframe,
  329. ondestroy: oneTrackDone
  330. });
  331. if (easing && easing !== 'spline') {
  332. clip.easing = easing;
  333. }
  334. return clip;
  335. }
  336. /**
  337. * @alias module:zrender/animation/Animator
  338. * @constructor
  339. * @param {Object} target
  340. * @param {boolean} loop
  341. * @param {Function} getter
  342. * @param {Function} setter
  343. */
  344. var Animator = function (target, loop, getter, setter) {
  345. this._tracks = {};
  346. this._target = target;
  347. this._loop = loop || false;
  348. this._getter = getter || defaultGetter;
  349. this._setter = setter || defaultSetter;
  350. this._clipCount = 0;
  351. this._delay = 0;
  352. this._doneList = [];
  353. this._onframeList = [];
  354. this._clipList = [];
  355. };
  356. Animator.prototype = {
  357. /**
  358. * Set Animation keyframe
  359. * @param {number} time 关键帧时间,单位是ms
  360. * @param {Object} props 关键帧的属性值,key-value表示
  361. * @return {module:zrender/animation/Animator}
  362. */
  363. when: function (time
  364. /* ms */
  365. , props) {
  366. var tracks = this._tracks;
  367. for (var propName in props) {
  368. if (!props.hasOwnProperty(propName)) {
  369. continue;
  370. }
  371. if (!tracks[propName]) {
  372. tracks[propName] = []; // Invalid value
  373. var value = this._getter(this._target, propName);
  374. if (value == null) {
  375. // zrLog('Invalid property ' + propName);
  376. continue;
  377. } // If time is 0
  378. // Then props is given initialize value
  379. // Else
  380. // Initialize value from current prop value
  381. if (time !== 0) {
  382. tracks[propName].push({
  383. time: 0,
  384. value: cloneValue(value)
  385. });
  386. }
  387. }
  388. tracks[propName].push({
  389. time: time,
  390. value: props[propName]
  391. });
  392. }
  393. return this;
  394. },
  395. /**
  396. * 添加动画每一帧的回调函数
  397. * @param {Function} callback
  398. * @return {module:zrender/animation/Animator}
  399. */
  400. during: function (callback) {
  401. this._onframeList.push(callback);
  402. return this;
  403. },
  404. pause: function () {
  405. for (var i = 0; i < this._clipList.length; i++) {
  406. this._clipList[i].pause();
  407. }
  408. this._paused = true;
  409. },
  410. resume: function () {
  411. for (var i = 0; i < this._clipList.length; i++) {
  412. this._clipList[i].resume();
  413. }
  414. this._paused = false;
  415. },
  416. isPaused: function () {
  417. return !!this._paused;
  418. },
  419. _doneCallback: function () {
  420. // Clear all tracks
  421. this._tracks = {}; // Clear all clips
  422. this._clipList.length = 0;
  423. var doneList = this._doneList;
  424. var len = doneList.length;
  425. for (var i = 0; i < len; i++) {
  426. doneList[i].call(this);
  427. }
  428. },
  429. /**
  430. * Start the animation
  431. * @param {string|Function} [easing]
  432. * 动画缓动函数,详见{@link module:zrender/animation/easing}
  433. * @param {boolean} forceAnimate
  434. * @return {module:zrender/animation/Animator}
  435. */
  436. start: function (easing, forceAnimate) {
  437. var self = this;
  438. var clipCount = 0;
  439. var oneTrackDone = function () {
  440. clipCount--;
  441. if (!clipCount) {
  442. self._doneCallback();
  443. }
  444. };
  445. var lastClip;
  446. for (var propName in this._tracks) {
  447. if (!this._tracks.hasOwnProperty(propName)) {
  448. continue;
  449. }
  450. var clip = createTrackClip(this, easing, oneTrackDone, this._tracks[propName], propName, forceAnimate);
  451. if (clip) {
  452. this._clipList.push(clip);
  453. clipCount++; // If start after added to animation
  454. if (this.animation) {
  455. this.animation.addClip(clip);
  456. }
  457. lastClip = clip;
  458. }
  459. } // Add during callback on the last clip
  460. if (lastClip) {
  461. var oldOnFrame = lastClip.onframe;
  462. lastClip.onframe = function (target, percent) {
  463. oldOnFrame(target, percent);
  464. for (var i = 0; i < self._onframeList.length; i++) {
  465. self._onframeList[i](target, percent);
  466. }
  467. };
  468. } // This optimization will help the case that in the upper application
  469. // the view may be refreshed frequently, where animation will be
  470. // called repeatly but nothing changed.
  471. if (!clipCount) {
  472. this._doneCallback();
  473. }
  474. return this;
  475. },
  476. /**
  477. * Stop animation
  478. * @param {boolean} forwardToLast If move to last frame before stop
  479. */
  480. stop: function (forwardToLast) {
  481. var clipList = this._clipList;
  482. var animation = this.animation;
  483. for (var i = 0; i < clipList.length; i++) {
  484. var clip = clipList[i];
  485. if (forwardToLast) {
  486. // Move to last frame before stop
  487. clip.onframe(this._target, 1);
  488. }
  489. animation && animation.removeClip(clip);
  490. }
  491. clipList.length = 0;
  492. },
  493. /**
  494. * Set when animation delay starts
  495. * @param {number} time 单位ms
  496. * @return {module:zrender/animation/Animator}
  497. */
  498. delay: function (time) {
  499. this._delay = time;
  500. return this;
  501. },
  502. /**
  503. * Add callback for animation end
  504. * @param {Function} cb
  505. * @return {module:zrender/animation/Animator}
  506. */
  507. done: function (cb) {
  508. if (cb) {
  509. this._doneList.push(cb);
  510. }
  511. return this;
  512. },
  513. /**
  514. * @return {Array.<module:zrender/animation/Clip>}
  515. */
  516. getClips: function () {
  517. return this._clipList;
  518. }
  519. };
  520. var _default = Animator;
  521. module.exports = _default;