| /** |
| * @module echarts/animation/Animator |
| */ |
| import Clip from './Clip'; |
| import * as color from '../tool/color'; |
| import { isArrayLike } from '../core/util'; |
| var arraySlice = Array.prototype.slice; |
| |
| function defaultGetter(target, key) { |
| return target[key]; |
| } |
| |
| function defaultSetter(target, key, value) { |
| target[key] = value; |
| } |
| /** |
| * @param {number} p0 |
| * @param {number} p1 |
| * @param {number} percent |
| * @return {number} |
| */ |
| |
| |
| function interpolateNumber(p0, p1, percent) { |
| return (p1 - p0) * percent + p0; |
| } |
| /** |
| * @param {string} p0 |
| * @param {string} p1 |
| * @param {number} percent |
| * @return {string} |
| */ |
| |
| |
| function interpolateString(p0, p1, percent) { |
| return percent > 0.5 ? p1 : p0; |
| } |
| /** |
| * @param {Array} p0 |
| * @param {Array} p1 |
| * @param {number} percent |
| * @param {Array} out |
| * @param {number} arrDim |
| */ |
| |
| |
| function interpolateArray(p0, p1, percent, out, arrDim) { |
| var len = p0.length; |
| |
| if (arrDim === 1) { |
| for (var i = 0; i < len; i++) { |
| out[i] = interpolateNumber(p0[i], p1[i], percent); |
| } |
| } else { |
| var len2 = len && p0[0].length; |
| |
| for (var i = 0; i < len; i++) { |
| for (var j = 0; j < len2; j++) { |
| out[i][j] = interpolateNumber(p0[i][j], p1[i][j], percent); |
| } |
| } |
| } |
| } // arr0 is source array, arr1 is target array. |
| // Do some preprocess to avoid error happened when interpolating from arr0 to arr1 |
| |
| |
| function fillArr(arr0, arr1, arrDim) { |
| var arr0Len = arr0.length; |
| var arr1Len = arr1.length; |
| |
| if (arr0Len !== arr1Len) { |
| // FIXME Not work for TypedArray |
| var isPreviousLarger = arr0Len > arr1Len; |
| |
| if (isPreviousLarger) { |
| // Cut the previous |
| arr0.length = arr1Len; |
| } else { |
| // Fill the previous |
| for (var i = arr0Len; i < arr1Len; i++) { |
| arr0.push(arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i])); |
| } |
| } |
| } // Handling NaN value |
| |
| |
| var len2 = arr0[0] && arr0[0].length; |
| |
| for (var i = 0; i < arr0.length; i++) { |
| if (arrDim === 1) { |
| if (isNaN(arr0[i])) { |
| arr0[i] = arr1[i]; |
| } |
| } else { |
| for (var j = 0; j < len2; j++) { |
| if (isNaN(arr0[i][j])) { |
| arr0[i][j] = arr1[i][j]; |
| } |
| } |
| } |
| } |
| } |
| /** |
| * @param {Array} arr0 |
| * @param {Array} arr1 |
| * @param {number} arrDim |
| * @return {boolean} |
| */ |
| |
| |
| function isArraySame(arr0, arr1, arrDim) { |
| if (arr0 === arr1) { |
| return true; |
| } |
| |
| var len = arr0.length; |
| |
| if (len !== arr1.length) { |
| return false; |
| } |
| |
| if (arrDim === 1) { |
| for (var i = 0; i < len; i++) { |
| if (arr0[i] !== arr1[i]) { |
| return false; |
| } |
| } |
| } else { |
| var len2 = arr0[0].length; |
| |
| for (var i = 0; i < len; i++) { |
| for (var j = 0; j < len2; j++) { |
| if (arr0[i][j] !== arr1[i][j]) { |
| return false; |
| } |
| } |
| } |
| } |
| |
| return true; |
| } |
| /** |
| * Catmull Rom interpolate array |
| * @param {Array} p0 |
| * @param {Array} p1 |
| * @param {Array} p2 |
| * @param {Array} p3 |
| * @param {number} t |
| * @param {number} t2 |
| * @param {number} t3 |
| * @param {Array} out |
| * @param {number} arrDim |
| */ |
| |
| |
| function catmullRomInterpolateArray(p0, p1, p2, p3, t, t2, t3, out, arrDim) { |
| var len = p0.length; |
| |
| if (arrDim === 1) { |
| for (var i = 0; i < len; i++) { |
| out[i] = catmullRomInterpolate(p0[i], p1[i], p2[i], p3[i], t, t2, t3); |
| } |
| } else { |
| var len2 = p0[0].length; |
| |
| for (var i = 0; i < len; i++) { |
| for (var j = 0; j < len2; j++) { |
| out[i][j] = catmullRomInterpolate(p0[i][j], p1[i][j], p2[i][j], p3[i][j], t, t2, t3); |
| } |
| } |
| } |
| } |
| /** |
| * Catmull Rom interpolate number |
| * @param {number} p0 |
| * @param {number} p1 |
| * @param {number} p2 |
| * @param {number} p3 |
| * @param {number} t |
| * @param {number} t2 |
| * @param {number} t3 |
| * @return {number} |
| */ |
| |
| |
| function catmullRomInterpolate(p0, p1, p2, p3, t, t2, t3) { |
| var v0 = (p2 - p0) * 0.5; |
| var v1 = (p3 - p1) * 0.5; |
| return (2 * (p1 - p2) + v0 + v1) * t3 + (-3 * (p1 - p2) - 2 * v0 - v1) * t2 + v0 * t + p1; |
| } |
| |
| function cloneValue(value) { |
| if (isArrayLike(value)) { |
| var len = value.length; |
| |
| if (isArrayLike(value[0])) { |
| var ret = []; |
| |
| for (var i = 0; i < len; i++) { |
| ret.push(arraySlice.call(value[i])); |
| } |
| |
| return ret; |
| } |
| |
| return arraySlice.call(value); |
| } |
| |
| return value; |
| } |
| |
| function rgba2String(rgba) { |
| rgba[0] = Math.floor(rgba[0]); |
| rgba[1] = Math.floor(rgba[1]); |
| rgba[2] = Math.floor(rgba[2]); |
| return 'rgba(' + rgba.join(',') + ')'; |
| } |
| |
| function getArrayDim(keyframes) { |
| var lastValue = keyframes[keyframes.length - 1].value; |
| return isArrayLike(lastValue && lastValue[0]) ? 2 : 1; |
| } |
| |
| function createTrackClip(animator, easing, oneTrackDone, keyframes, propName, forceAnimate) { |
| var getter = animator._getter; |
| var setter = animator._setter; |
| var useSpline = easing === 'spline'; |
| var trackLen = keyframes.length; |
| |
| if (!trackLen) { |
| return; |
| } // Guess data type |
| |
| |
| var firstVal = keyframes[0].value; |
| var isValueArray = isArrayLike(firstVal); |
| var isValueColor = false; |
| var isValueString = false; // For vertices morphing |
| |
| var arrDim = isValueArray ? getArrayDim(keyframes) : 0; |
| var trackMaxTime; // Sort keyframe as ascending |
| |
| keyframes.sort(function (a, b) { |
| return a.time - b.time; |
| }); |
| trackMaxTime = keyframes[trackLen - 1].time; // Percents of each keyframe |
| |
| var kfPercents = []; // Value of each keyframe |
| |
| var kfValues = []; |
| var prevValue = keyframes[0].value; |
| var isAllValueEqual = true; |
| |
| for (var i = 0; i < trackLen; i++) { |
| kfPercents.push(keyframes[i].time / trackMaxTime); // Assume value is a color when it is a string |
| |
| var value = keyframes[i].value; // Check if value is equal, deep check if value is array |
| |
| if (!(isValueArray && isArraySame(value, prevValue, arrDim) || !isValueArray && value === prevValue)) { |
| isAllValueEqual = false; |
| } |
| |
| prevValue = value; // Try converting a string to a color array |
| |
| if (typeof value === 'string') { |
| var colorArray = color.parse(value); |
| |
| if (colorArray) { |
| value = colorArray; |
| isValueColor = true; |
| } else { |
| isValueString = true; |
| } |
| } |
| |
| kfValues.push(value); |
| } |
| |
| if (!forceAnimate && isAllValueEqual) { |
| return; |
| } |
| |
| var lastValue = kfValues[trackLen - 1]; // Polyfill array and NaN value |
| |
| for (var i = 0; i < trackLen - 1; i++) { |
| if (isValueArray) { |
| fillArr(kfValues[i], lastValue, arrDim); |
| } else { |
| if (isNaN(kfValues[i]) && !isNaN(lastValue) && !isValueString && !isValueColor) { |
| kfValues[i] = lastValue; |
| } |
| } |
| } |
| |
| isValueArray && fillArr(getter(animator._target, propName), lastValue, arrDim); // Cache the key of last frame to speed up when |
| // animation playback is sequency |
| |
| var lastFrame = 0; |
| var lastFramePercent = 0; |
| var start; |
| var w; |
| var p0; |
| var p1; |
| var p2; |
| var p3; |
| |
| if (isValueColor) { |
| var rgba = [0, 0, 0, 0]; |
| } |
| |
| var onframe = function (target, percent) { |
| // Find the range keyframes |
| // kf1-----kf2---------current--------kf3 |
| // find kf2 and kf3 and do interpolation |
| var frame; // In the easing function like elasticOut, percent may less than 0 |
| |
| if (percent < 0) { |
| frame = 0; |
| } else if (percent < lastFramePercent) { |
| // Start from next key |
| // PENDING start from lastFrame ? |
| start = Math.min(lastFrame + 1, trackLen - 1); |
| |
| for (frame = start; frame >= 0; frame--) { |
| if (kfPercents[frame] <= percent) { |
| break; |
| } |
| } // PENDING really need to do this ? |
| |
| |
| frame = Math.min(frame, trackLen - 2); |
| } else { |
| for (frame = lastFrame; frame < trackLen; frame++) { |
| if (kfPercents[frame] > percent) { |
| break; |
| } |
| } |
| |
| frame = Math.min(frame - 1, trackLen - 2); |
| } |
| |
| lastFrame = frame; |
| lastFramePercent = percent; |
| var range = kfPercents[frame + 1] - kfPercents[frame]; |
| |
| if (range === 0) { |
| return; |
| } else { |
| w = (percent - kfPercents[frame]) / range; |
| } |
| |
| if (useSpline) { |
| p1 = kfValues[frame]; |
| p0 = kfValues[frame === 0 ? frame : frame - 1]; |
| p2 = kfValues[frame > trackLen - 2 ? trackLen - 1 : frame + 1]; |
| p3 = kfValues[frame > trackLen - 3 ? trackLen - 1 : frame + 2]; |
| |
| if (isValueArray) { |
| catmullRomInterpolateArray(p0, p1, p2, p3, w, w * w, w * w * w, getter(target, propName), arrDim); |
| } else { |
| var value; |
| |
| if (isValueColor) { |
| value = catmullRomInterpolateArray(p0, p1, p2, p3, w, w * w, w * w * w, rgba, 1); |
| value = rgba2String(rgba); |
| } else if (isValueString) { |
| // String is step(0.5) |
| return interpolateString(p1, p2, w); |
| } else { |
| value = catmullRomInterpolate(p0, p1, p2, p3, w, w * w, w * w * w); |
| } |
| |
| setter(target, propName, value); |
| } |
| } else { |
| if (isValueArray) { |
| interpolateArray(kfValues[frame], kfValues[frame + 1], w, getter(target, propName), arrDim); |
| } else { |
| var value; |
| |
| if (isValueColor) { |
| interpolateArray(kfValues[frame], kfValues[frame + 1], w, rgba, 1); |
| value = rgba2String(rgba); |
| } else if (isValueString) { |
| // String is step(0.5) |
| return interpolateString(kfValues[frame], kfValues[frame + 1], w); |
| } else { |
| value = interpolateNumber(kfValues[frame], kfValues[frame + 1], w); |
| } |
| |
| setter(target, propName, value); |
| } |
| } |
| }; |
| |
| var clip = new Clip({ |
| target: animator._target, |
| life: trackMaxTime, |
| loop: animator._loop, |
| delay: animator._delay, |
| onframe: onframe, |
| ondestroy: oneTrackDone |
| }); |
| |
| if (easing && easing !== 'spline') { |
| clip.easing = easing; |
| } |
| |
| return clip; |
| } |
| /** |
| * @alias module:zrender/animation/Animator |
| * @constructor |
| * @param {Object} target |
| * @param {boolean} loop |
| * @param {Function} getter |
| * @param {Function} setter |
| */ |
| |
| |
| var Animator = function (target, loop, getter, setter) { |
| this._tracks = {}; |
| this._target = target; |
| this._loop = loop || false; |
| this._getter = getter || defaultGetter; |
| this._setter = setter || defaultSetter; |
| this._clipCount = 0; |
| this._delay = 0; |
| this._doneList = []; |
| this._onframeList = []; |
| this._clipList = []; |
| }; |
| |
| Animator.prototype = { |
| /** |
| * Set Animation keyframe |
| * @param {number} time 关键帧时间,单位是ms |
| * @param {Object} props 关键帧的属性值,key-value表示 |
| * @return {module:zrender/animation/Animator} |
| */ |
| when: function (time |
| /* ms */ |
| , props) { |
| var tracks = this._tracks; |
| |
| for (var propName in props) { |
| if (!props.hasOwnProperty(propName)) { |
| continue; |
| } |
| |
| if (!tracks[propName]) { |
| tracks[propName] = []; // Invalid value |
| |
| var value = this._getter(this._target, propName); |
| |
| if (value == null) { |
| // zrLog('Invalid property ' + propName); |
| continue; |
| } // If time is 0 |
| // Then props is given initialize value |
| // Else |
| // Initialize value from current prop value |
| |
| |
| if (time !== 0) { |
| tracks[propName].push({ |
| time: 0, |
| value: cloneValue(value) |
| }); |
| } |
| } |
| |
| tracks[propName].push({ |
| time: time, |
| value: props[propName] |
| }); |
| } |
| |
| return this; |
| }, |
| |
| /** |
| * 添加动画每一帧的回调函数 |
| * @param {Function} callback |
| * @return {module:zrender/animation/Animator} |
| */ |
| during: function (callback) { |
| this._onframeList.push(callback); |
| |
| return this; |
| }, |
| pause: function () { |
| for (var i = 0; i < this._clipList.length; i++) { |
| this._clipList[i].pause(); |
| } |
| |
| this._paused = true; |
| }, |
| resume: function () { |
| for (var i = 0; i < this._clipList.length; i++) { |
| this._clipList[i].resume(); |
| } |
| |
| this._paused = false; |
| }, |
| isPaused: function () { |
| return !!this._paused; |
| }, |
| _doneCallback: function () { |
| // Clear all tracks |
| this._tracks = {}; // Clear all clips |
| |
| this._clipList.length = 0; |
| var doneList = this._doneList; |
| var len = doneList.length; |
| |
| for (var i = 0; i < len; i++) { |
| doneList[i].call(this); |
| } |
| }, |
| |
| /** |
| * Start the animation |
| * @param {string|Function} [easing] |
| * 动画缓动函数,详见{@link module:zrender/animation/easing} |
| * @param {boolean} forceAnimate |
| * @return {module:zrender/animation/Animator} |
| */ |
| start: function (easing, forceAnimate) { |
| var self = this; |
| var clipCount = 0; |
| |
| var oneTrackDone = function () { |
| clipCount--; |
| |
| if (!clipCount) { |
| self._doneCallback(); |
| } |
| }; |
| |
| var lastClip; |
| |
| for (var propName in this._tracks) { |
| if (!this._tracks.hasOwnProperty(propName)) { |
| continue; |
| } |
| |
| var clip = createTrackClip(this, easing, oneTrackDone, this._tracks[propName], propName, forceAnimate); |
| |
| if (clip) { |
| this._clipList.push(clip); |
| |
| clipCount++; // If start after added to animation |
| |
| if (this.animation) { |
| this.animation.addClip(clip); |
| } |
| |
| lastClip = clip; |
| } |
| } // Add during callback on the last clip |
| |
| |
| if (lastClip) { |
| var oldOnFrame = lastClip.onframe; |
| |
| lastClip.onframe = function (target, percent) { |
| oldOnFrame(target, percent); |
| |
| for (var i = 0; i < self._onframeList.length; i++) { |
| self._onframeList[i](target, percent); |
| } |
| }; |
| } // This optimization will help the case that in the upper application |
| // the view may be refreshed frequently, where animation will be |
| // called repeatly but nothing changed. |
| |
| |
| if (!clipCount) { |
| this._doneCallback(); |
| } |
| |
| return this; |
| }, |
| |
| /** |
| * Stop animation |
| * @param {boolean} forwardToLast If move to last frame before stop |
| */ |
| stop: function (forwardToLast) { |
| var clipList = this._clipList; |
| var animation = this.animation; |
| |
| for (var i = 0; i < clipList.length; i++) { |
| var clip = clipList[i]; |
| |
| if (forwardToLast) { |
| // Move to last frame before stop |
| clip.onframe(this._target, 1); |
| } |
| |
| animation && animation.removeClip(clip); |
| } |
| |
| clipList.length = 0; |
| }, |
| |
| /** |
| * Set when animation delay starts |
| * @param {number} time 单位ms |
| * @return {module:zrender/animation/Animator} |
| */ |
| delay: function (time) { |
| this._delay = time; |
| return this; |
| }, |
| |
| /** |
| * Add callback for animation end |
| * @param {Function} cb |
| * @return {module:zrender/animation/Animator} |
| */ |
| done: function (cb) { |
| if (cb) { |
| this._doneList.push(cb); |
| } |
| |
| return this; |
| }, |
| |
| /** |
| * @return {Array.<module:zrender/animation/Clip>} |
| */ |
| getClips: function () { |
| return this._clipList; |
| } |
| }; |
| export default Animator; |