/** 
 | 
 * @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; 
 |