var Clip = require("./Clip"); var color = require("../tool/color"); var _util = require("../core/util"); var isArrayLike = _util.isArrayLike; /** * @module echarts/animation/Animator */ 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.} */ getClips: function () { return this._clipList; } }; var _default = Animator; module.exports = _default;