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