zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
1,149 lines (1,017 loc) • 35.5 kB
text/typescript
/**
* @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;