You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							644 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
	
	
							644 lines
						
					
					
						
							14 KiB
						
					
					
				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 = { | 
						|
  /** | 
						|
   * 设置动画关键帧 | 
						|
   * @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); | 
						|
    } | 
						|
  }, | 
						|
 | 
						|
  /** | 
						|
   * 开始执行动画 | 
						|
   * @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; | 
						|
  }, | 
						|
 | 
						|
  /** | 
						|
   * 停止动画 | 
						|
   * @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; | 
						|
  }, | 
						|
 | 
						|
  /** | 
						|
   * 设置动画延迟开始的时间 | 
						|
   * @param  {number} time 单位ms | 
						|
   * @return {module:zrender/animation/Animator} | 
						|
   */ | 
						|
  delay: function (time) { | 
						|
    this._delay = time; | 
						|
    return this; | 
						|
  }, | 
						|
 | 
						|
  /** | 
						|
   * 添加动画结束的回调 | 
						|
   * @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; | 
						|
  } | 
						|
}; | 
						|
var _default = Animator; | 
						|
module.exports = _default; |