// Definitions. import { Keyframes as KeyframesDefinition, UnparsedKeyframe as UnparsedKeyframeDefinition, KeyframeDeclaration as KeyframeDeclarationDefinition, KeyframeInfo as KeyframeInfoDefinition, KeyframeAnimationInfo as KeyframeAnimationInfoDefinition, KeyframeAnimation as KeyframeAnimationDefinition, } from "./keyframe-animation"; import { View, Color } from "../core/view"; import { AnimationCurve } from "../enums"; import { write as traceWrite, categories as traceCategories, messageType as traceType } from "../../trace"; // Types. import { unsetValue } from "../core/properties"; import { Animation } from "./animation"; import { backgroundColorProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, rotateProperty, opacityProperty, widthProperty, heightProperty, PercentLength } from "../styling/style-properties"; export class Keyframes implements KeyframesDefinition { name: string; keyframes: Array; } export class UnparsedKeyframe implements UnparsedKeyframeDefinition { values: Array; declarations: Array; } export class KeyframeDeclaration implements KeyframeDeclarationDefinition { public property: string; public value: any; } export class KeyframeInfo implements KeyframeInfoDefinition { public duration: number; public declarations: Array; public curve?: any = AnimationCurve.ease; } export class KeyframeAnimationInfo implements KeyframeAnimationInfoDefinition { public keyframes: Array; public name?: string = ""; public duration?: number = 0.3; public delay?: number = 0; public iterations?: number = 1; public curve?: any = "ease"; public isForwards?: boolean = false; public isReverse?: boolean = false; } interface Keyframe { backgroundColor?: Color; scale?: { x: number, y: number }; translate?: { x: number, y: number }; rotate?: number; opacity?: number; width?: PercentLength; height?: PercentLength; valueSource?: "keyframe" | "animation"; duration?: number; curve?: any; forceLayer?: boolean; } export class KeyframeAnimation implements KeyframeAnimationDefinition { public animations: Array; public delay: number = 0; public iterations: number = 1; private _resolve; private _isPlaying: boolean; private _isForwards: boolean; private _nativeAnimations: Array; private _target: View; public static keyframeAnimationFromInfo(info: KeyframeAnimationInfo) : KeyframeAnimation { const length = info.keyframes.length; let animations = new Array(); let startDuration = 0; if (info.isReverse) { for (let index = length - 1; index >= 0; index--) { let keyframe = info.keyframes[index]; startDuration = KeyframeAnimation.parseKeyframe(info, keyframe, animations, startDuration); } } else { for (let index = 0; index < length; index++) { let keyframe = info.keyframes[index]; startDuration = KeyframeAnimation.parseKeyframe(info, keyframe, animations, startDuration); } for (let index = length - 1; index > 0; index--) { let a1 = animations[index]; let a2 = animations[index - 1]; if (a2["curve"] !== undefined) { a1["curve"] = a2["curve"]; a2["curve"] = undefined; } } } animations.map(a => a["curve"] ? a : Object.assign(a, { curve: info.curve })); const animation: KeyframeAnimation = new KeyframeAnimation(); animation.delay = info.delay; animation.iterations = info.iterations; animation.animations = animations; animation._isForwards = info.isForwards; return animation; } private static parseKeyframe(info: KeyframeAnimationInfo, keyframe: KeyframeInfo, animations: Array, startDuration: number): number { let animation: Keyframe = {}; for (let declaration of keyframe.declarations) { animation[declaration.property] = declaration.value; } let duration = keyframe.duration; if (duration === 0) { duration = 0.01; } else { duration = (info.duration * duration) - startDuration; startDuration += duration; } animation.duration = info.isReverse ? info.duration - duration : duration; animation.curve = keyframe.curve; animation.forceLayer = true; animation.valueSource = "keyframe"; animations.push(animation); return startDuration; } public get isPlaying(): boolean { return this._isPlaying; } public cancel() { if (!this.isPlaying) { traceWrite("Keyframe animation is already playing.", traceCategories.Animation, traceType.warn); return; } this._isPlaying = false; for (let i = this._nativeAnimations.length - 1; i >= 0; i--) { let animation = this._nativeAnimations[i]; if (animation.isPlaying) { animation.cancel(); } } if (this._nativeAnimations.length > 0) { let animation = this._nativeAnimations[0]; this._resetAnimationValues(this._target, animation); } this._resetAnimations(); } public play(view: View): Promise { if (this._isPlaying) { traceWrite("Keyframe animation is already playing.", traceCategories.Animation, traceType.warn); return new Promise(resolve => { resolve(); }); } let animationFinishedPromise = new Promise(resolve => { this._resolve = resolve; }); this._isPlaying = true; this._nativeAnimations = new Array(); this._target = view; if (this.delay !== 0) { setTimeout(() => this.animate(view, 0, this.iterations), this.delay); } else { this.animate(view, 0, this.iterations); } return animationFinishedPromise; } private animate(view: View, index: number, iterations: number) { if (!this._isPlaying) { return; } if (index === 0) { let animation = this.animations[0]; if ("backgroundColor" in animation) { view.style[backgroundColorProperty.keyframe] = animation.backgroundColor; } if ("scale" in animation) { view.style[scaleXProperty.keyframe] = animation.scale.x; view.style[scaleYProperty.keyframe] = animation.scale.y; } if ("translate" in animation) { view.style[translateXProperty.keyframe] = animation.translate.x; view.style[translateYProperty.keyframe] = animation.translate.y; } if ("rotate" in animation) { view.style[rotateProperty.keyframe] = animation.rotate; } if ("opacity" in animation) { view.style[opacityProperty.keyframe] = animation.opacity; } if ("height" in animation) { view.style[heightProperty.keyframe] = animation.height; } if ("width" in animation) { view.style[widthProperty.keyframe] = animation.width; } setTimeout(() => this.animate(view, 1, iterations), 1); } else if (index < 0 || index >= this.animations.length) { iterations -= 1; if (iterations > 0) { this.animate(view, 0, iterations); } else { if (this._isForwards === false) { let animation = this.animations[this.animations.length - 1]; this._resetAnimationValues(view, animation); } this._resolveAnimationFinishedPromise(); } } else { let animation; const cachedAnimation = this._nativeAnimations[index - 1]; if (cachedAnimation) { animation = cachedAnimation; } else { let animationDef = this.animations[index]; (animationDef).target = view; animation = new Animation([animationDef]); this._nativeAnimations.push(animation); } const isLastIteration = iterations - 1 <= 0; // Catch the animation cancel to prevent unhandled promise rejection warnings animation.play(isLastIteration).then(() => { this.animate(view, index + 1, iterations); }, (error: any) => { traceWrite(typeof error === "string" ? error : error.message, traceCategories.Animation, traceType.warn); }).catch((error: any) => { traceWrite(typeof error === "string" ? error : error.message, traceCategories.Animation, traceType.warn); }); // tslint:disable-line } } public _resolveAnimationFinishedPromise() { this._nativeAnimations = new Array(); this._isPlaying = false; this._target = null; this._resolve(); } public _resetAnimations() { this._nativeAnimations = new Array(); this._isPlaying = false; this._target = null; } private _resetAnimationValues(view: View, animation: Object) { if ("backgroundColor" in animation) { view.style[backgroundColorProperty.keyframe] = unsetValue; } if ("scale" in animation) { view.style[scaleXProperty.keyframe] = unsetValue; view.style[scaleYProperty.keyframe] = unsetValue; } if ("translate" in animation) { view.style[translateXProperty.keyframe] = unsetValue; view.style[translateYProperty.keyframe] = unsetValue; } if ("rotate" in animation) { view.style[rotateProperty.keyframe] = unsetValue; } if ("opacity" in animation) { view.style[opacityProperty.keyframe] = unsetValue; } if ("height" in animation) { view.style[heightProperty.keyframe] = unsetValue; } if ("width" in animation) { view.style[widthProperty.keyframe] = unsetValue; } } }