| /** | 
|  * @module echarts/animation/Animator | 
|  */ | 
|   | 
| import Clip from './Clip'; | 
| import * as color from '../tool/color'; | 
| import { | 
|     eqNaN, | 
|     extend, | 
|     isArrayLike, | 
|     isFunction, | 
|     isGradientObject, | 
|     isNumber, | 
|     isString, | 
|     keys, | 
|     logError, | 
|     map | 
| } from '../core/util'; | 
| import {ArrayLike, Dictionary} from '../core/types'; | 
| import easingFuncs, { AnimationEasing } from './easing'; | 
| import Animation from './Animation'; | 
| import { createCubicEasingFunc } from './cubicEasing'; | 
| import { isLinearGradient, isRadialGradient } from '../svg/helper'; | 
|   | 
| type NumberArray = ArrayLike<number> | 
| type InterpolatableType = string | number | NumberArray | NumberArray[]; | 
|   | 
| interface ParsedColorStop { | 
|     color: number[], | 
|     offset: number | 
| }; | 
|   | 
| interface ParsedGradientObject { | 
|     colorStops: ParsedColorStop[] | 
|     x: number | 
|     y: number | 
|     global: boolean | 
| } | 
| interface ParsedLinearGradientObject extends ParsedGradientObject { | 
|     x2: number | 
|     y2: number | 
| } | 
| interface ParsedRadialGradientObject extends ParsedGradientObject { | 
|     r: number | 
| } | 
|   | 
| const arraySlice = Array.prototype.slice; | 
|   | 
| function interpolateNumber(p0: number, p1: number, percent: number): number { | 
|     return (p1 - p0) * percent + p0; | 
| } | 
| function interpolate1DArray( | 
|     out: NumberArray, | 
|     p0: NumberArray, | 
|     p1: NumberArray, | 
|     percent: number | 
| ) { | 
|     // TODO Handling different length TypedArray | 
|     const len = p0.length; | 
|     for (let i = 0; i < len; i++) { | 
|         out[i] = interpolateNumber(p0[i], p1[i], percent); | 
|     } | 
|     return out; | 
| } | 
|   | 
| function interpolate2DArray( | 
|     out: NumberArray[], | 
|     p0: NumberArray[], | 
|     p1: NumberArray[], | 
|     percent: number | 
| ) { | 
|     const len = p0.length; | 
|     // TODO differnt length on each item? | 
|     const len2 = len && p0[0].length; | 
|     for (let i = 0; i < len; i++) { | 
|         if (!out[i]) { | 
|             out[i] = []; | 
|         } | 
|         for (let j = 0; j < len2; j++) { | 
|             out[i][j] = interpolateNumber(p0[i][j], p1[i][j], percent); | 
|         } | 
|     } | 
|     return out; | 
| } | 
|   | 
| function add1DArray( | 
|     out: NumberArray, | 
|     p0: NumberArray, | 
|     p1: NumberArray, | 
|     sign: 1 | -1 | 
| ) { | 
|     const len = p0.length; | 
|     for (let i = 0; i < len; i++) { | 
|         out[i] = p0[i] + p1[i] * sign; | 
|     } | 
|     return out; | 
| } | 
|   | 
| function add2DArray( | 
|     out: NumberArray[], | 
|     p0: NumberArray[], | 
|     p1: NumberArray[], | 
|     sign: 1 | -1 | 
| ) { | 
|     const len = p0.length; | 
|     const len2 = len && p0[0].length; | 
|     for (let i = 0; i < len; i++) { | 
|         if (!out[i]) { | 
|             out[i] = []; | 
|         } | 
|         for (let j = 0; j < len2; j++) { | 
|             out[i][j] = p0[i][j] + p1[i][j] * sign; | 
|         } | 
|     } | 
|     return out; | 
| } | 
|   | 
| function fillColorStops(val0: ParsedColorStop[], val1: ParsedColorStop[]) { | 
|     const len0 = val0.length; | 
|     const len1 = val1.length; | 
|   | 
|     const shorterArr = len0 > len1 ? val1 : val0; | 
|     const shorterLen = Math.min(len0, len1); | 
|     const last = shorterArr[shorterLen - 1] || { color: [0, 0, 0, 0], offset: 0 }; | 
|     for (let i = shorterLen; i < Math.max(len0, len1); i++) { | 
|         // Use last color stop to fill the shorter array | 
|         shorterArr.push({ | 
|             offset: last.offset, | 
|             color: last.color.slice() | 
|         }); | 
|     } | 
| } | 
| // arr0 is source array, arr1 is target array. | 
| // Do some preprocess to avoid error happened when interpolating from arr0 to arr1 | 
| function fillArray( | 
|     val0: NumberArray | NumberArray[], | 
|     val1: NumberArray | NumberArray[], | 
|     arrDim: 1 | 2 | 
| ) { | 
|     // TODO Handling different length TypedArray | 
|     let arr0 = val0 as (number | number[])[]; | 
|     let arr1 = val1 as (number | number[])[]; | 
|     if (!arr0.push || !arr1.push) { | 
|         return; | 
|     } | 
|     const arr0Len = arr0.length; | 
|     const arr1Len = arr1.length; | 
|     if (arr0Len !== arr1Len) { | 
|         // FIXME Not work for TypedArray | 
|         const isPreviousLarger = arr0Len > arr1Len; | 
|         if (isPreviousLarger) { | 
|             // Cut the previous | 
|             arr0.length = arr1Len; | 
|         } | 
|         else { | 
|             // Fill the previous | 
|             for (let i = arr0Len; i < arr1Len; i++) { | 
|                 arr0.push(arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i])); | 
|             } | 
|         } | 
|     } | 
|     // Handling NaN value | 
|     const len2 = arr0[0] && (arr0[0] as number[]).length; | 
|     for (let i = 0; i < arr0.length; i++) { | 
|         if (arrDim === 1) { | 
|             if (isNaN(arr0[i] as number)) { | 
|                 arr0[i] = arr1[i]; | 
|             } | 
|         } | 
|         else { | 
|             for (let j = 0; j < len2; j++) { | 
|                 if (isNaN((arr0 as number[][])[i][j])) { | 
|                     (arr0 as number[][])[i][j] = (arr1 as number[][])[i][j]; | 
|                 } | 
|             } | 
|         } | 
|     } | 
| } | 
|   | 
| export function cloneValue(value: InterpolatableType) { | 
|     if (isArrayLike(value)) { | 
|         const len = value.length; | 
|         if (isArrayLike(value[0])) { | 
|             const ret = []; | 
|             for (let i = 0; i < len; i++) { | 
|                 ret.push(arraySlice.call(value[i])); | 
|             } | 
|             return ret; | 
|         } | 
|   | 
|         return arraySlice.call(value); | 
|     } | 
|   | 
|     return value; | 
| } | 
|   | 
| function rgba2String(rgba: number[]): string { | 
|     rgba[0] = Math.floor(rgba[0]) || 0; | 
|     rgba[1] = Math.floor(rgba[1]) || 0; | 
|     rgba[2] = Math.floor(rgba[2]) || 0; | 
|     rgba[3] = rgba[3] == null ? 1 : rgba[3]; | 
|   | 
|     return 'rgba(' + rgba.join(',') + ')'; | 
| } | 
|   | 
| function guessArrayDim(value: ArrayLike<unknown>): 1 | 2 { | 
|     return isArrayLike(value && (value as ArrayLike<unknown>)[0]) ? 2 : 1; | 
| } | 
|   | 
| const VALUE_TYPE_NUMBER = 0; | 
| const VALUE_TYPE_1D_ARRAY = 1; | 
| const VALUE_TYPE_2D_ARRAY = 2; | 
| const VALUE_TYPE_COLOR = 3; | 
| const VALUE_TYPE_LINEAR_GRADIENT = 4; | 
| const VALUE_TYPE_RADIAL_GRADIENT = 5; | 
| // Other value type that can only use discrete animation. | 
| const VALUE_TYPE_UNKOWN = 6; | 
|   | 
| type ValueType = 0 | 1 | 2 | 3 | 4 | 5 | 6; | 
|   | 
| type Keyframe = { | 
|     time: number | 
|     value: unknown | 
|     percent: number | 
|     // Raw value for discrete animation. | 
|     rawValue: unknown | 
|   | 
|     easing?: AnimationEasing    // Raw easing | 
|     easingFunc?: (percent: number) => number | 
|     additiveValue?: unknown | 
| } | 
|   | 
|   | 
| function isGradientValueType(valType: ValueType): valType is 4 | 5 { | 
|     return valType === VALUE_TYPE_LINEAR_GRADIENT || valType === VALUE_TYPE_RADIAL_GRADIENT; | 
| } | 
| function isArrayValueType(valType: ValueType): valType is 1 | 2 { | 
|     return valType === VALUE_TYPE_1D_ARRAY || valType === VALUE_TYPE_2D_ARRAY; | 
| } | 
|   | 
|   | 
| let tmpRgba: number[] = [0, 0, 0, 0]; | 
|   | 
| class Track { | 
|   | 
|     keyframes: Keyframe[] = [] | 
|   | 
|     propName: string | 
|   | 
|     valType: ValueType | 
|   | 
|     discrete: boolean = false | 
|   | 
|     _invalid: boolean = false; | 
|   | 
|     private _finished: boolean | 
|   | 
|     private _needsSort: boolean = false | 
|   | 
|     private _additiveTrack: Track | 
|     // Temporal storage for interpolated additive value. | 
|     private _additiveValue: unknown | 
|   | 
|     // Info for run | 
|     /** | 
|      * Last frame | 
|      */ | 
|     private _lastFr = 0 | 
|     /** | 
|      * Percent of last frame. | 
|      */ | 
|     private _lastFrP = 0 | 
|   | 
|     constructor(propName: string) { | 
|         this.propName = propName; | 
|     } | 
|   | 
|     isFinished() { | 
|         return this._finished; | 
|     } | 
|   | 
|     setFinished() { | 
|         this._finished = true; | 
|         // Also set additive track to finished. | 
|         // Make sure the final value stopped on the latest track | 
|         if (this._additiveTrack) { | 
|             this._additiveTrack.setFinished(); | 
|         } | 
|     } | 
|   | 
|     needsAnimate() { | 
|         return this.keyframes.length >= 1; | 
|     } | 
|   | 
|     getAdditiveTrack() { | 
|         return this._additiveTrack; | 
|     } | 
|   | 
|     addKeyframe(time: number, rawValue: unknown, easing?: AnimationEasing) { | 
|         this._needsSort = true; | 
|   | 
|         let keyframes = this.keyframes; | 
|         let len = keyframes.length; | 
|   | 
|         let discrete = false; | 
|         let valType: ValueType = VALUE_TYPE_UNKOWN; | 
|         let value = rawValue; | 
|   | 
|         // Handling values only if it's possible to be interpolated. | 
|         if (isArrayLike(rawValue)) { | 
|             let arrayDim = guessArrayDim(rawValue); | 
|             valType = arrayDim; | 
|             // Not a number array. | 
|             if (arrayDim === 1 && !isNumber(rawValue[0]) | 
|                 || arrayDim === 2 && !isNumber(rawValue[0][0])) { | 
|                 discrete = true; | 
|             } | 
|         } | 
|         else { | 
|             if (isNumber(rawValue) && !eqNaN(rawValue)) { | 
|                 valType = VALUE_TYPE_NUMBER; | 
|             } | 
|             else if (isString(rawValue)) { | 
|                 if (!isNaN(+rawValue)) {    // Can be string number like '2' | 
|                     valType = VALUE_TYPE_NUMBER; | 
|                 } | 
|                 else { | 
|                     const colorArray = color.parse(rawValue); | 
|                     if (colorArray) { | 
|                         value = colorArray; | 
|                         valType = VALUE_TYPE_COLOR; | 
|                     } | 
|                 } | 
|             } | 
|             else if (isGradientObject(rawValue)) { | 
|                 // TODO Color to gradient or gradient to color. | 
|                 const parsedGradient = extend({}, value) as unknown as ParsedGradientObject; | 
|                 parsedGradient.colorStops = map(rawValue.colorStops, colorStop => ({ | 
|                     offset: colorStop.offset, | 
|                     color: color.parse(colorStop.color) | 
|                 })); | 
|                 if (isLinearGradient(rawValue)) { | 
|                     valType = VALUE_TYPE_LINEAR_GRADIENT; | 
|                 } | 
|                 else if (isRadialGradient(rawValue)) { | 
|                     valType = VALUE_TYPE_RADIAL_GRADIENT; | 
|                 } | 
|                 value = parsedGradient; | 
|             } | 
|         } | 
|   | 
|         if (len === 0) { | 
|             // Inference type from the first keyframe. | 
|             this.valType = valType; | 
|         } | 
|          // Not same value type or can't be interpolated. | 
|         else if (valType !== this.valType || valType === VALUE_TYPE_UNKOWN) { | 
|             discrete = true; | 
|         } | 
|   | 
|         this.discrete = this.discrete || discrete; | 
|   | 
|         const kf: Keyframe = { | 
|             time, | 
|             value, | 
|             rawValue, | 
|             percent: 0 | 
|         }; | 
|         if (easing) { | 
|             // Save the raw easing name to be used in css animation output | 
|             kf.easing = easing; | 
|             kf.easingFunc = isFunction(easing) | 
|                 ? easing | 
|                 : easingFuncs[easing] || createCubicEasingFunc(easing); | 
|         } | 
|         // Not check if value equal here. | 
|         keyframes.push(kf); | 
|         return kf; | 
|     } | 
|   | 
|     prepare(maxTime: number, additiveTrack?: Track) { | 
|         let kfs = this.keyframes; | 
|         if (this._needsSort) { | 
|             // Sort keyframe as ascending | 
|             kfs.sort(function (a: Keyframe, b: Keyframe) { | 
|                 return a.time - b.time; | 
|             }); | 
|         } | 
|   | 
|         const valType = this.valType; | 
|         const kfsLen = kfs.length; | 
|         const lastKf = kfs[kfsLen - 1]; | 
|         const isDiscrete = this.discrete; | 
|         const isArr = isArrayValueType(valType); | 
|         const isGradient = isGradientValueType(valType); | 
|   | 
|         for (let i = 0; i < kfsLen; i++) { | 
|             const kf = kfs[i]; | 
|             const value = kf.value; | 
|             const lastValue = lastKf.value; | 
|             kf.percent = kf.time / maxTime; | 
|             if (!isDiscrete) { | 
|                 if (isArr && i !== kfsLen - 1) { | 
|                     // Align array with target frame. | 
|                     fillArray(value as NumberArray, lastValue as NumberArray, valType); | 
|                 } | 
|                 else if (isGradient) { | 
|                     fillColorStops( | 
|                         (value as ParsedLinearGradientObject).colorStops, | 
|                         (lastValue as ParsedLinearGradientObject).colorStops | 
|                     ); | 
|                 } | 
|             } | 
|         } | 
|   | 
|         // Only apply additive animaiton on INTERPOLABLE SAME TYPE values. | 
|         if ( | 
|             !isDiscrete | 
|             // TODO support gradient | 
|             && valType !== VALUE_TYPE_RADIAL_GRADIENT | 
|             && additiveTrack | 
|             // If two track both will be animated and have same value format. | 
|             && this.needsAnimate() | 
|             && additiveTrack.needsAnimate() | 
|             && valType === additiveTrack.valType | 
|             && !additiveTrack._finished | 
|         ) { | 
|             this._additiveTrack = additiveTrack; | 
|   | 
|             const startValue = kfs[0].value; | 
|             // Calculate difference | 
|             for (let i = 0; i < kfsLen; i++) { | 
|                 if (valType === VALUE_TYPE_NUMBER) { | 
|                     kfs[i].additiveValue = kfs[i].value as number - (startValue as number); | 
|                 } | 
|                 else if (valType === VALUE_TYPE_COLOR) { | 
|                     kfs[i].additiveValue = | 
|                         add1DArray([], kfs[i].value as NumberArray, startValue as NumberArray, -1); | 
|                 } | 
|                 else if (isArrayValueType(valType)) { | 
|                     kfs[i].additiveValue = valType === VALUE_TYPE_1D_ARRAY | 
|                         ? add1DArray([], kfs[i].value as NumberArray, startValue as NumberArray, -1) | 
|                         : add2DArray([], kfs[i].value as NumberArray[], startValue as NumberArray[], -1); | 
|                 } | 
|             } | 
|         } | 
|     } | 
|   | 
|     step(target: any, percent: number) { | 
|         if (this._finished) {   // Track may be set to finished. | 
|             return; | 
|         } | 
|   | 
|         if (this._additiveTrack && this._additiveTrack._finished) { | 
|             // Remove additive track if it's finished. | 
|             this._additiveTrack = null; | 
|         } | 
|         const isAdditive = this._additiveTrack != null; | 
|         const valueKey = isAdditive ? 'additiveValue' : 'value'; | 
|   | 
|         const valType = this.valType; | 
|         const keyframes = this.keyframes; | 
|         const kfsNum = keyframes.length; | 
|         const propName = this.propName; | 
|         const isValueColor = valType === VALUE_TYPE_COLOR; | 
|         // Find the range keyframes | 
|         // kf1-----kf2---------current--------kf3 | 
|         // find kf2 and kf3 and do interpolation | 
|         let frameIdx; | 
|         const lastFrame = this._lastFr; | 
|         const mathMin = Math.min; | 
|         let frame; | 
|         let nextFrame; | 
|         if (kfsNum === 1) { | 
|             frame = nextFrame = keyframes[0]; | 
|         } | 
|         else { | 
|             // In the easing function like elasticOut, percent may less than 0 | 
|             if (percent < 0) { | 
|                 frameIdx = 0; | 
|             } | 
|             else if (percent < this._lastFrP) { | 
|                 // Start from next key | 
|                 // PENDING start from lastFrame ? | 
|                 const start = mathMin(lastFrame + 1, kfsNum - 1); | 
|                 for (frameIdx = start; frameIdx >= 0; frameIdx--) { | 
|                     if (keyframes[frameIdx].percent <= percent) { | 
|                         break; | 
|                     } | 
|                 } | 
|                 frameIdx = mathMin(frameIdx, kfsNum - 2); | 
|             } | 
|             else { | 
|                 for (frameIdx = lastFrame; frameIdx < kfsNum; frameIdx++) { | 
|                     if (keyframes[frameIdx].percent > percent) { | 
|                         break; | 
|                     } | 
|                 } | 
|                 frameIdx = mathMin(frameIdx - 1, kfsNum - 2); | 
|             } | 
|   | 
|             nextFrame = keyframes[frameIdx + 1]; | 
|             frame = keyframes[frameIdx]; | 
|         } | 
|   | 
|         // Defensive coding. | 
|         if (!(frame && nextFrame)) { | 
|             return; | 
|         } | 
|   | 
|         this._lastFr = frameIdx; | 
|         this._lastFrP = percent; | 
|   | 
|         const interval = (nextFrame.percent - frame.percent); | 
|         let w = interval === 0 ? 1 : mathMin((percent - frame.percent) / interval, 1); | 
|   | 
|         // Apply different easing of each keyframe. | 
|         // Use easing specified in target frame. | 
|         if (nextFrame.easingFunc) { | 
|             w = nextFrame.easingFunc(w); | 
|         } | 
|   | 
|         // If value is arr | 
|         let targetArr = isAdditive ? this._additiveValue | 
|             : (isValueColor ? tmpRgba : target[propName]); | 
|   | 
|         if ((isArrayValueType(valType) || isValueColor) && !targetArr) { | 
|             targetArr = this._additiveValue = []; | 
|         } | 
|   | 
|         if (this.discrete) { | 
|             // use raw value without parse in discrete animation. | 
|             target[propName] = w < 1 ? frame.rawValue : nextFrame.rawValue; | 
|         } | 
|         else if (isArrayValueType(valType)) { | 
|             valType === VALUE_TYPE_1D_ARRAY | 
|                 ? interpolate1DArray( | 
|                     targetArr as NumberArray, | 
|                     frame[valueKey] as NumberArray, | 
|                     nextFrame[valueKey] as NumberArray, | 
|                     w | 
|                 ) | 
|                 : interpolate2DArray( | 
|                     targetArr as NumberArray[], | 
|                     frame[valueKey] as NumberArray[], | 
|                     nextFrame[valueKey] as NumberArray[], | 
|                     w | 
|                 ); | 
|         } | 
|         else if (isGradientValueType(valType)) { | 
|             const val = frame[valueKey] as ParsedGradientObject; | 
|             const nextVal = nextFrame[valueKey] as ParsedGradientObject; | 
|             const isLinearGradient = valType === VALUE_TYPE_LINEAR_GRADIENT; | 
|             target[propName] = { | 
|                 type: isLinearGradient ? 'linear' : 'radial', | 
|                 x: interpolateNumber(val.x, nextVal.x, w), | 
|                 y: interpolateNumber(val.y, nextVal.y, w), | 
|                 // TODO performance | 
|                 colorStops: map(val.colorStops, (colorStop, idx) => { | 
|                     const nextColorStop = nextVal.colorStops[idx]; | 
|                     return { | 
|                         offset: interpolateNumber(colorStop.offset, nextColorStop.offset, w), | 
|                         color: rgba2String(interpolate1DArray( | 
|                             [] as number[], colorStop.color, nextColorStop.color, w | 
|                         ) as number[]) | 
|                     }; | 
|                 }), | 
|                 global: nextVal.global | 
|             }; | 
|             if (isLinearGradient) { | 
|                 // Linear | 
|                 target[propName].x2 = interpolateNumber( | 
|                     (val as ParsedLinearGradientObject).x2, (nextVal as ParsedLinearGradientObject).x2, w | 
|                 ); | 
|                 target[propName].y2 = interpolateNumber( | 
|                     (val as ParsedLinearGradientObject).y2, (nextVal as ParsedLinearGradientObject).y2, w | 
|                 ); | 
|             } | 
|             else { | 
|                 // Radial | 
|                 target[propName].r = interpolateNumber( | 
|                     (val as ParsedRadialGradientObject).r, (nextVal as ParsedRadialGradientObject).r, w | 
|                 ); | 
|             } | 
|         } | 
|         else if (isValueColor) { | 
|             interpolate1DArray( | 
|                 targetArr, | 
|                 frame[valueKey] as NumberArray, | 
|                 nextFrame[valueKey] as NumberArray, | 
|                 w | 
|             ); | 
|             if (!isAdditive) {  // Convert to string later:) | 
|                 target[propName] = rgba2String(targetArr); | 
|             } | 
|         } | 
|         else { | 
|             const value = interpolateNumber(frame[valueKey] as number, nextFrame[valueKey] as number, w); | 
|             if (isAdditive) { | 
|                 this._additiveValue = value; | 
|             } | 
|             else { | 
|                 target[propName] = value; | 
|             } | 
|         } | 
|   | 
|         // Add additive to target | 
|         if (isAdditive) { | 
|             this._addToTarget(target); | 
|         } | 
|     } | 
|   | 
|     private _addToTarget(target: any) { | 
|         const valType = this.valType; | 
|         const propName = this.propName; | 
|         const additiveValue = this._additiveValue; | 
|   | 
|         if (valType === VALUE_TYPE_NUMBER) { | 
|             // Add a difference value based on the change of previous frame. | 
|             target[propName] = target[propName] + additiveValue; | 
|         } | 
|         else if (valType === VALUE_TYPE_COLOR) { | 
|             // TODO reduce unnecessary parse | 
|             color.parse(target[propName], tmpRgba); | 
|             add1DArray(tmpRgba, tmpRgba, additiveValue as NumberArray, 1); | 
|             target[propName] = rgba2String(tmpRgba); | 
|         } | 
|         else if (valType === VALUE_TYPE_1D_ARRAY) { | 
|             add1DArray(target[propName], target[propName], additiveValue as NumberArray, 1); | 
|         } | 
|         else if (valType === VALUE_TYPE_2D_ARRAY) { | 
|             add2DArray(target[propName], target[propName], additiveValue as NumberArray[], 1); | 
|         } | 
|     } | 
| } | 
|   | 
|   | 
| type DoneCallback = () => void; | 
| type AbortCallback = () => void; | 
| export type OnframeCallback<T> = (target: T, percent: number) => void; | 
|   | 
| export type AnimationPropGetter<T> = (target: T, key: string) => InterpolatableType; | 
| export type AnimationPropSetter<T> = (target: T, key: string, value: InterpolatableType) => void; | 
|   | 
| export default class Animator<T> { | 
|   | 
|     animation?: Animation | 
|   | 
|     targetName?: string | 
|   | 
|     scope?: string | 
|   | 
|     __fromStateTransition?: string | 
|   | 
|     private _tracks: Dictionary<Track> = {} | 
|     private _trackKeys: string[] = [] | 
|   | 
|     private _target: T | 
|   | 
|     private _loop: boolean | 
|     private _delay: number | 
|     private _maxTime = 0 | 
|   | 
|     /** | 
|      * If force run regardless of empty tracks when duration is set. | 
|      */ | 
|     private _force: boolean; | 
|   | 
|     /** | 
|      * If animator is paused | 
|      * @default false | 
|      */ | 
|     private _paused: boolean | 
|     // 0: Not started | 
|     // 1: Invoked started | 
|     // 2: Has been run for at least one frame. | 
|     private _started = 0 | 
|   | 
|     /** | 
|      * If allow discrete animation | 
|      * @default false | 
|      */ | 
|     private _allowDiscrete: boolean | 
|   | 
|     private _additiveAnimators: Animator<any>[] | 
|   | 
|     private _doneCbs: DoneCallback[] | 
|     private _onframeCbs: OnframeCallback<T>[] | 
|   | 
|     private _abortedCbs: AbortCallback[] | 
|   | 
|     private _clip: Clip = null | 
|   | 
|     constructor( | 
|         target: T, | 
|         loop: boolean, | 
|         allowDiscreteAnimation?: boolean,  // If doing discrete animation on the values can't be interpolated | 
|         additiveTo?: Animator<any>[] | 
|     ) { | 
|         this._target = target; | 
|         this._loop = loop; | 
|         if (loop && additiveTo) { | 
|             logError('Can\' use additive animation on looped animation.'); | 
|             return; | 
|         } | 
|         this._additiveAnimators = additiveTo; | 
|   | 
|         this._allowDiscrete = allowDiscreteAnimation; | 
|     } | 
|   | 
|     getMaxTime() { | 
|         return this._maxTime; | 
|     } | 
|   | 
|     getDelay() { | 
|         return this._delay; | 
|     } | 
|   | 
|     getLoop() { | 
|         return this._loop; | 
|     } | 
|   | 
|     getTarget() { | 
|         return this._target; | 
|     } | 
|   | 
|     /** | 
|      * Target can be changed during animation | 
|      * For example if style is changed during state change. | 
|      * We need to change target to the new style object. | 
|      */ | 
|     changeTarget(target: T) { | 
|         this._target = target; | 
|     } | 
|   | 
|     /** | 
|      * Set Animation keyframe | 
|      * @param time time of keyframe in ms | 
|      * @param props key-value props of keyframe. | 
|      * @param easing | 
|      */ | 
|     when(time: number, props: Dictionary<any>, easing?: AnimationEasing) { | 
|         return this.whenWithKeys(time, props, keys(props) as string[], easing); | 
|     } | 
|   | 
|   | 
|     // Fast path for add keyframes of aniamteTo | 
|     whenWithKeys(time: number, props: Dictionary<any>, propNames: string[], easing?: AnimationEasing) { | 
|         const tracks = this._tracks; | 
|         for (let i = 0; i < propNames.length; i++) { | 
|             const propName = propNames[i]; | 
|   | 
|             let track = tracks[propName]; | 
|             if (!track) { | 
|                 track = tracks[propName] = new Track(propName); | 
|   | 
|                 let initialValue; | 
|                 const additiveTrack = this._getAdditiveTrack(propName); | 
|                 if (additiveTrack) { | 
|                     const addtiveTrackKfs = additiveTrack.keyframes; | 
|                     const lastFinalKf = addtiveTrackKfs[addtiveTrackKfs.length - 1]; | 
|                     // Use the last state of additived animator. | 
|                     initialValue = lastFinalKf && lastFinalKf.value; | 
|                     if (additiveTrack.valType === VALUE_TYPE_COLOR && initialValue) { | 
|                         // Convert to rgba string | 
|                         initialValue = rgba2String(initialValue as number[]); | 
|                     } | 
|                 } | 
|                 else { | 
|                     initialValue = (this._target as any)[propName]; | 
|                 } | 
|                 // Invalid value | 
|                 if (initialValue == null) { | 
|                     // zrLog('Invalid property ' + propName); | 
|                     continue; | 
|                 } | 
|                 // If time is <= 0 | 
|                 //  Then props is given initialize value | 
|                 //  Note: initial percent can be negative, which means the initial value is before the animation start. | 
|                 // Else | 
|                 //  Initialize value from current prop value | 
|                 if (time > 0) { | 
|                     track.addKeyframe(0, cloneValue(initialValue), easing); | 
|                 } | 
|   | 
|                 this._trackKeys.push(propName); | 
|             } | 
|             track.addKeyframe(time, cloneValue(props[propName]), easing); | 
|         } | 
|         this._maxTime = Math.max(this._maxTime, time); | 
|         return this; | 
|     } | 
|   | 
|     pause() { | 
|         this._clip.pause(); | 
|         this._paused = true; | 
|     } | 
|   | 
|     resume() { | 
|         this._clip.resume(); | 
|         this._paused = false; | 
|     } | 
|   | 
|     isPaused(): boolean { | 
|         return !!this._paused; | 
|     } | 
|   | 
|     /** | 
|      * Set duration of animator. | 
|      * Will run this duration regardless the track max time or if trackes exits. | 
|      * @param duration | 
|      * @returns | 
|      */ | 
|     duration(duration: number) { | 
|         this._maxTime = duration; | 
|         this._force = true; | 
|         return this; | 
|     } | 
|   | 
|     private _doneCallback() { | 
|         this._setTracksFinished(); | 
|         // Clear clip | 
|         this._clip = null; | 
|   | 
|         const doneList = this._doneCbs; | 
|         if (doneList) { | 
|             const len = doneList.length; | 
|             for (let i = 0; i < len; i++) { | 
|                 doneList[i].call(this); | 
|             } | 
|         } | 
|     } | 
|     private _abortedCallback() { | 
|         this._setTracksFinished(); | 
|   | 
|         const animation = this.animation; | 
|         const abortedList = this._abortedCbs; | 
|   | 
|         if (animation) { | 
|             animation.removeClip(this._clip); | 
|         } | 
|         this._clip = null; | 
|   | 
|         if (abortedList) { | 
|             for (let i = 0; i < abortedList.length; i++) { | 
|                 abortedList[i].call(this); | 
|             } | 
|         } | 
|     } | 
|     private _setTracksFinished() { | 
|         const tracks = this._tracks; | 
|         const tracksKeys = this._trackKeys; | 
|         for (let i = 0; i < tracksKeys.length; i++) { | 
|             tracks[tracksKeys[i]].setFinished(); | 
|         } | 
|     } | 
|   | 
|     private _getAdditiveTrack(trackName: string): Track { | 
|         let additiveTrack; | 
|         const additiveAnimators = this._additiveAnimators; | 
|         if (additiveAnimators) { | 
|             for (let i = 0; i < additiveAnimators.length; i++) { | 
|                 const track = additiveAnimators[i].getTrack(trackName); | 
|                 if (track) { | 
|                     // Use the track of latest animator. | 
|                     additiveTrack = track; | 
|                 } | 
|             } | 
|         } | 
|         return additiveTrack; | 
|     } | 
|   | 
|     /** | 
|      * Start the animation | 
|      * @param easing | 
|      * @return | 
|      */ | 
|     start(easing?: AnimationEasing) { | 
|         if (this._started > 0) { | 
|             return; | 
|         } | 
|         this._started = 1; | 
|   | 
|         const self = this; | 
|   | 
|         const tracks: Track[] = []; | 
|         const maxTime = this._maxTime || 0; | 
|         for (let i = 0; i < this._trackKeys.length; i++) { | 
|             const propName = this._trackKeys[i]; | 
|             const track = this._tracks[propName]; | 
|             const additiveTrack = this._getAdditiveTrack(propName); | 
|             const kfs = track.keyframes; | 
|             const kfsNum = kfs.length; | 
|             track.prepare(maxTime, additiveTrack); | 
|             if (track.needsAnimate()) { | 
|                 // Set value directly if discrete animation is not allowed. | 
|                 if (!this._allowDiscrete && track.discrete) { | 
|                     const lastKf = kfs[kfsNum - 1]; | 
|                     // Set final value. | 
|                     if (lastKf) { | 
|                         // use raw value without parse. | 
|                         (self._target as any)[track.propName] = lastKf.rawValue; | 
|                     } | 
|                     track.setFinished(); | 
|                 } | 
|                 else { | 
|                     tracks.push(track); | 
|                 } | 
|             } | 
|         } | 
|         // Add during callback on the last clip | 
|         if (tracks.length || this._force) { | 
|             const clip = new Clip({ | 
|                 life: maxTime, | 
|                 loop: this._loop, | 
|                 delay: this._delay || 0, | 
|                 onframe(percent: number) { | 
|                     self._started = 2; | 
|                     // Remove additived animator if it's finished. | 
|                     // For the purpose of memory effeciency. | 
|                     const additiveAnimators = self._additiveAnimators; | 
|                     if (additiveAnimators) { | 
|                         let stillHasAdditiveAnimator = false; | 
|                         for (let i = 0; i < additiveAnimators.length; i++) { | 
|                             if (additiveAnimators[i]._clip) { | 
|                                 stillHasAdditiveAnimator = true; | 
|                                 break; | 
|                             } | 
|                         } | 
|                         if (!stillHasAdditiveAnimator) { | 
|                             self._additiveAnimators = null; | 
|                         } | 
|                     } | 
|   | 
|                     for (let i = 0; i < tracks.length; i++) { | 
|                         // NOTE: don't cache target outside. | 
|                         // Because target may be changed. | 
|                         tracks[i].step(self._target, percent); | 
|                     } | 
|   | 
|                     const onframeList = self._onframeCbs; | 
|                     if (onframeList) { | 
|                         for (let i = 0; i < onframeList.length; i++) { | 
|                             onframeList[i](self._target, percent); | 
|                         } | 
|                     } | 
|                 }, | 
|                 ondestroy() { | 
|                     self._doneCallback(); | 
|                 } | 
|             }); | 
|             this._clip = clip; | 
|   | 
|             if (this.animation) { | 
|                 this.animation.addClip(clip); | 
|             } | 
|   | 
|             if (easing) { | 
|                 clip.setEasing(easing); | 
|             } | 
|         } | 
|         else { | 
|             // 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. | 
|             this._doneCallback(); | 
|         } | 
|   | 
|         return this; | 
|     } | 
|     /** | 
|      * Stop animation | 
|      * @param {boolean} forwardToLast If move to last frame before stop | 
|      */ | 
|     stop(forwardToLast?: boolean) { | 
|         if (!this._clip) { | 
|             return; | 
|         } | 
|         const clip = this._clip; | 
|         if (forwardToLast) { | 
|             // Move to last frame before stop | 
|             clip.onframe(1); | 
|         } | 
|   | 
|         this._abortedCallback(); | 
|     } | 
|     /** | 
|      * Set when animation delay starts | 
|      * @param time 单位ms | 
|      */ | 
|     delay(time: number) { | 
|         this._delay = time; | 
|         return this; | 
|     } | 
|     /** | 
|      * 添加动画每一帧的回调函数 | 
|      * @param callback | 
|      */ | 
|     during(cb: OnframeCallback<T>) { | 
|         if (cb) { | 
|             if (!this._onframeCbs) { | 
|                 this._onframeCbs = []; | 
|             } | 
|             this._onframeCbs.push(cb); | 
|         } | 
|         return this; | 
|     } | 
|     /** | 
|      * Add callback for animation end | 
|      * @param cb | 
|      */ | 
|     done(cb: DoneCallback) { | 
|         if (cb) { | 
|             if (!this._doneCbs) { | 
|                 this._doneCbs = []; | 
|             } | 
|             this._doneCbs.push(cb); | 
|         } | 
|         return this; | 
|     } | 
|   | 
|     aborted(cb: AbortCallback) { | 
|         if (cb) { | 
|             if (!this._abortedCbs) { | 
|                 this._abortedCbs = []; | 
|             } | 
|             this._abortedCbs.push(cb); | 
|         } | 
|         return this; | 
|     } | 
|   | 
|     getClip() { | 
|         return this._clip; | 
|     } | 
|   | 
|     getTrack(propName: string) { | 
|         return this._tracks[propName]; | 
|     } | 
|   | 
|     getTracks() { | 
|         return map(this._trackKeys, key => this._tracks[key]); | 
|     } | 
|   | 
|     /** | 
|      * Return true if animator is not available anymore. | 
|      */ | 
|     stopTracks(propNames: string[], forwardToLast?: boolean): boolean { | 
|         if (!propNames.length || !this._clip) { | 
|             return true; | 
|         } | 
|         const tracks = this._tracks; | 
|         const tracksKeys = this._trackKeys; | 
|   | 
|         for (let i = 0; i < propNames.length; i++) { | 
|             const track = tracks[propNames[i]]; | 
|             if (track && !track.isFinished()) { | 
|                 if (forwardToLast) { | 
|                     track.step(this._target, 1); | 
|                 } | 
|                 // If the track has not been run for at least one frame. | 
|                 // The property may be stayed at the final state. when setToFinal is set true. | 
|                 // For example: | 
|                 // Animate x from 0 to 100, then animate to 150 immediately. | 
|                 // We want the x is translated from 0 to 150, not 100 to 150. | 
|                 else if (this._started === 1) { | 
|                     track.step(this._target, 0); | 
|                 } | 
|                 // Set track to finished | 
|                 track.setFinished(); | 
|             } | 
|         } | 
|         let allAborted = true; | 
|         for (let i = 0; i < tracksKeys.length; i++) { | 
|             if (!tracks[tracksKeys[i]].isFinished()) { | 
|                 allAborted = false; | 
|                 break; | 
|             } | 
|         } | 
|         // Remove clip if all tracks has been aborted. | 
|         if (allAborted) { | 
|             this._abortedCallback(); | 
|         } | 
|   | 
|         return allAborted; | 
|     } | 
|   | 
|     /** | 
|      * Save values of final state to target. | 
|      * It is mainly used in state mangement. When state is switching during animation. | 
|      * We need to save final state of animation to the normal state. Not interpolated value. | 
|      * | 
|      * @param target | 
|      * @param trackKeys | 
|      * @param firstOrLast If save first frame or last frame | 
|      */ | 
|     saveTo( | 
|         target: T, | 
|         trackKeys?: readonly string[], | 
|         firstOrLast?: boolean | 
|     ) { | 
|         if (!target) {  // DO nothing if target is not given. | 
|             return; | 
|         } | 
|   | 
|         trackKeys = trackKeys || this._trackKeys; | 
|   | 
|         for (let i = 0; i < trackKeys.length; i++) { | 
|             const propName = trackKeys[i]; | 
|             const track = this._tracks[propName]; | 
|             if (!track || track.isFinished()) {   // Ignore finished track. | 
|                 continue; | 
|             } | 
|             const kfs = track.keyframes; | 
|             const kf = kfs[firstOrLast ? 0 : kfs.length - 1]; | 
|             if (kf) { | 
|                 // TODO CLONE? | 
|                 // Use raw value without parse. | 
|                 (target as any)[propName] = cloneValue(kf.rawValue as any); | 
|             } | 
|         } | 
|     } | 
|   | 
|     // Change final value after animator has been started. | 
|     // NOTE: Be careful to use it. | 
|     __changeFinalValue(finalProps: Dictionary<any>, trackKeys?: readonly string[]) { | 
|         trackKeys = trackKeys || keys(finalProps); | 
|   | 
|         for (let i = 0; i < trackKeys.length; i++) { | 
|             const propName = trackKeys[i]; | 
|   | 
|             const track = this._tracks[propName]; | 
|             if (!track) { | 
|                 continue; | 
|             } | 
|   | 
|             const kfs = track.keyframes; | 
|             if (kfs.length > 1) { | 
|                 // Remove the original last kf and add again. | 
|                 const lastKf = kfs.pop(); | 
|   | 
|                 track.addKeyframe(lastKf.time, finalProps[propName]); | 
|                 // Prepare again. | 
|                 track.prepare(this._maxTime, track.getAdditiveTrack()); | 
|             } | 
|         } | 
|     } | 
| } | 
|   | 
| export type AnimatorTrack = Track; |