feat(core): introduce support for animations

Closes #8734
This commit is contained in:
Matias Niemelä
2016-05-25 12:46:22 -07:00
parent 6c6b316bd9
commit 5e0f8cf3f0
83 changed files with 5294 additions and 756 deletions

View File

@ -0,0 +1,57 @@
import {AnimationPlayer} from './animation_player';
import {isPresent} from '../facade/lang';
import {ListWrapper, StringMapWrapper, Map} from '../facade/collection';
export class ActiveAnimationPlayersMap {
private _map = new Map<any, {[key: string]: AnimationPlayer}>();
private _allPlayers: AnimationPlayer[] = [];
get length(): number {
return this.getAllPlayers().length;
}
find(element: any, animationName: string): AnimationPlayer {
var playersByAnimation = this._map.get(element);
if (isPresent(playersByAnimation)) {
return playersByAnimation[animationName];
}
}
findAllPlayersByElement(element: any): AnimationPlayer[] {
var players = [];
StringMapWrapper.forEach(this._map.get(element), player => players.push(player));
return players;
}
set(element: any, animationName: string, player: AnimationPlayer): void {
var playersByAnimation = this._map.get(element);
if (!isPresent(playersByAnimation)) {
playersByAnimation = {};
}
var existingEntry = playersByAnimation[animationName];
if (isPresent(existingEntry)) {
this.remove(element, animationName);
}
playersByAnimation[animationName] = player;
this._allPlayers.push(player);
this._map.set(element, playersByAnimation);
}
getAllPlayers(): AnimationPlayer[] {
return this._allPlayers;
}
remove(element: any, animationName: string): void {
var playersByAnimation = this._map.get(element);
if (isPresent(playersByAnimation)) {
var player = playersByAnimation[animationName];
delete playersByAnimation[animationName];
var index = this._allPlayers.indexOf(player);
ListWrapper.removeAt(this._allPlayers, index);
if (StringMapWrapper.isEmpty(playersByAnimation)) {
this._map.delete(element);
}
}
}
}

View File

@ -0,0 +1,3 @@
export const FILL_STYLE_FLAG = 'true'; // TODO (matsko): change to boolean
export const ANY_STATE = '*';
export const EMPTY_STATE = 'void';

View File

@ -0,0 +1,15 @@
import {NoOpAnimationPlayer, AnimationPlayer} from './animation_player';
import {AnimationKeyframe} from './animation_keyframe';
import {AnimationStyles} from './animation_styles';
export abstract class AnimationDriver {
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer;
}
export class NoOpAnimationDriver extends AnimationDriver {
animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer {
return new NoOpAnimationPlayer();
}
}

View File

@ -0,0 +1,72 @@
import {AnimationPlayer} from './animation_player';
import {isPresent, scheduleMicroTask} from '../facade/lang';
import {Math} from '../facade/math';
export class AnimationGroupPlayer implements AnimationPlayer {
private _subscriptions: Function[] = [];
private _finished = false;
public parentPlayer: AnimationPlayer = null;
constructor(private _players: AnimationPlayer[]) {
var count = 0;
var total = this._players.length;
if (total == 0) {
scheduleMicroTask(() => this._onFinish());
} else {
this._players.forEach(player => {
player.parentPlayer = this;
player.onDone(() => {
if (++count >= total) {
this._onFinish();
}
});
});
}
}
private _onFinish() {
if (!this._finished) {
this._finished = true;
if (!isPresent(this.parentPlayer)) {
this.destroy();
}
this._subscriptions.forEach(subscription => subscription());
this._subscriptions = [];
}
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play() { this._players.forEach(player => player.play()); }
pause(): void { this._players.forEach(player => player.pause()); }
restart(): void { this._players.forEach(player => player.restart()); }
finish(): void {
this._onFinish();
this._players.forEach(player => player.finish());
}
destroy(): void {
this._onFinish();
this._players.forEach(player => player.destroy());
}
reset(): void { this._players.forEach(player => player.reset()); }
setPosition(p): void {
this._players.forEach(player => {
player.setPosition(p);
});
}
getPosition(): number {
var min = 0;
this._players.forEach(player => {
var p = player.getPosition();
min = Math.min(p, min);
});
return min;
}
}

View File

@ -0,0 +1,5 @@
import {AnimationStyles} from './animation_styles';
export class AnimationKeyframe {
constructor(public offset: number, public styles: AnimationStyles) {}
}

View File

@ -0,0 +1,39 @@
import {scheduleMicroTask} from '../facade/lang';
import {BaseException} from '../facade/exceptions';
export abstract class AnimationPlayer {
abstract onDone(fn: Function): void;
abstract play(): void;
abstract pause(): void;
abstract restart(): void;
abstract finish(): void;
abstract destroy(): void;
abstract reset(): void;
abstract setPosition(p): void;
abstract getPosition(): number;
get parentPlayer(): AnimationPlayer { throw new BaseException('NOT IMPLEMENTED: Base Class'); }
set parentPlayer(player: AnimationPlayer) { throw new BaseException('NOT IMPLEMENTED: Base Class'); }
}
export class NoOpAnimationPlayer implements AnimationPlayer {
private _subscriptions = [];
public parentPlayer: AnimationPlayer = null;
constructor() {
scheduleMicroTask(() => this._onFinish());
}
_onFinish() {
this._subscriptions.forEach(entry => { entry(); });
this._subscriptions = [];
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void {}
pause(): void {}
restart(): void {}
finish(): void {
this._onFinish();
}
destroy(): void {}
reset(): void {}
setPosition(p): void {}
getPosition(): number { return 0; }
}

View File

@ -0,0 +1,83 @@
import {isPresent} from '../facade/lang';
import {NoOpAnimationPlayer, AnimationPlayer} from './animation_player';
import {scheduleMicroTask} from '../facade/lang';
export class AnimationSequencePlayer implements AnimationPlayer {
private _currentIndex: number = 0;
private _activePlayer: AnimationPlayer;
private _subscriptions: Function[] = [];
private _finished = false;
public parentPlayer: AnimationPlayer = null;
constructor(private _players: AnimationPlayer[]) {
this._players.forEach(player => {
player.parentPlayer = this;
});
this._onNext(false);
}
private _onNext(start: boolean) {
if (this._finished) return;
if (this._players.length == 0) {
this._activePlayer = new NoOpAnimationPlayer();
scheduleMicroTask(() => this._onFinish());
} else if (this._currentIndex >= this._players.length) {
this._activePlayer = new NoOpAnimationPlayer();
this._onFinish();
} else {
var player = this._players[this._currentIndex++];
player.onDone(() => this._onNext(true));
this._activePlayer = player;
if (start) {
player.play();
}
}
}
private _onFinish() {
if (!this._finished) {
this._finished = true;
if (!isPresent(this.parentPlayer)) {
this.destroy();
}
this._subscriptions.forEach(subscription => subscription());
this._subscriptions = [];
}
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void { this._activePlayer.play(); }
pause(): void { this._activePlayer.pause(); }
restart(): void {
if (this._players.length > 0) {
this.reset();
this._players[0].restart();
}
}
reset(): void { this._players.forEach(player => player.reset()); }
finish(): void {
this._onFinish();
this._players.forEach(player => player.finish());
}
destroy(): void {
this._onFinish();
this._players.forEach(player => player.destroy());
}
setPosition(p): void {
this._players[0].setPosition(p);
}
getPosition(): number {
return this._players[0].getPosition();
}
}

View File

@ -0,0 +1,113 @@
import {isPresent, isArray} from '../facade/lang';
import {ListWrapper, StringMapWrapper} from '../facade/collection';
import {AUTO_STYLE} from './metadata';
import {FILL_STYLE_FLAG} from './animation_constants';
export class AnimationStyleUtil {
static balanceStyles(previousStyles: {[key: string]: string|number},
newStyles: {[key: string]: string|number},
nullValue = null): {[key: string]: string|number} {
var finalStyles: {[key: string]: string|number} = {};
StringMapWrapper.forEach(newStyles, (value, prop) => {
finalStyles[prop] = value;
});
StringMapWrapper.forEach(previousStyles, (value, prop) => {
if (!isPresent(finalStyles[prop])) {
finalStyles[prop] = nullValue;
}
});
return finalStyles;
}
static balanceKeyframes(collectedStyles: {[key: string]: string|number},
finalStateStyles: {[key: string]: string|number},
keyframes: any[]): any[] {
var limit = keyframes.length - 1;
var firstKeyframe = keyframes[0];
// phase 1: copy all the styles from the first keyframe into the lookup map
var flatenedFirstKeyframeStyles = AnimationStyleUtil.flattenStyles(firstKeyframe.styles.styles);
var extraFirstKeyframeStyles = {};
var hasExtraFirstStyles = false;
StringMapWrapper.forEach(collectedStyles, (value, prop) => {
// if the style is already defined in the first keyframe then
// we do not replace it.
if (!flatenedFirstKeyframeStyles[prop]) {
flatenedFirstKeyframeStyles[prop] = value;
extraFirstKeyframeStyles[prop] = value;
hasExtraFirstStyles = true;
}
});
var keyframeCollectedStyles = StringMapWrapper.merge({}, flatenedFirstKeyframeStyles);
// phase 2: normalize the final keyframe
var finalKeyframe = keyframes[limit];
ListWrapper.insert(finalKeyframe.styles.styles, 0, finalStateStyles);
var flatenedFinalKeyframeStyles = AnimationStyleUtil.flattenStyles(finalKeyframe.styles.styles);
var extraFinalKeyframeStyles = {};
var hasExtraFinalStyles = false;
StringMapWrapper.forEach(keyframeCollectedStyles, (value, prop) => {
if (!isPresent(flatenedFinalKeyframeStyles[prop])) {
extraFinalKeyframeStyles[prop] = AUTO_STYLE;
hasExtraFinalStyles = true;
}
});
if (hasExtraFinalStyles) {
finalKeyframe.styles.styles.push(extraFinalKeyframeStyles);
}
StringMapWrapper.forEach(flatenedFinalKeyframeStyles, (value, prop) => {
if (!isPresent(flatenedFirstKeyframeStyles[prop])) {
extraFirstKeyframeStyles[prop] = AUTO_STYLE;
hasExtraFirstStyles = true;
}
});
if (hasExtraFirstStyles) {
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
}
return keyframes;
}
static clearStyles(styles: {[key: string]: string|number}): {[key: string]: string|number} {
var finalStyles: {[key: string]: string|number} = {};
StringMapWrapper.keys(styles).forEach(key => {
finalStyles[key] = null;
});
return finalStyles;
}
static collectAndResolveStyles(collection: {[key: string]: string|number}, styles: {[key: string]: string|number}[]) {
return styles.map(entry => {
var stylesObj = {};
StringMapWrapper.forEach(entry, (value, prop) => {
if (value == FILL_STYLE_FLAG) {
value = collection[prop];
if (!isPresent(value)) {
value = AUTO_STYLE;
}
}
collection[prop] = value;
stylesObj[prop] = value;
});
return stylesObj;
});
}
static flattenStyles(styles: {[key: string]: string|number}[]) {
var finalStyles = {};
styles.forEach(entry => {
StringMapWrapper.forEach(entry, (value, prop) => {
finalStyles[prop] = value;
});
});
return finalStyles;
}
}

View File

@ -0,0 +1,3 @@
export class AnimationStyles {
constructor(public styles: {[key: string]: string | number}[]) {}
}

View File

@ -0,0 +1,116 @@
import {isPresent, isArray, isString, isStringMap, NumberWrapper} from '../facade/lang';
import {BaseException} from '../facade/exceptions';
export const AUTO_STYLE = "*";
export class AnimationEntryMetadata {
constructor(public name: string, public definitions: AnimationStateMetadata[]) {}
}
export abstract class AnimationStateMetadata {}
export class AnimationStateDeclarationMetadata extends AnimationStateMetadata {
constructor(public stateNameExpr: string, public styles: AnimationStyleMetadata) { super(); }
}
export class AnimationStateTransitionMetadata extends AnimationStateMetadata {
constructor(public stateChangeExpr: string, public animation: AnimationMetadata) { super(); }
}
export abstract class AnimationMetadata {}
export class AnimationKeyframesSequenceMetadata extends AnimationMetadata {
constructor(public steps: AnimationStyleMetadata[]) {
super();
}
}
export class AnimationStyleMetadata extends AnimationMetadata {
constructor(public styles: Array<string|{[key: string]: string | number}>, public offset: number = null) { super(); }
}
export class AnimationAnimateMetadata extends AnimationMetadata {
constructor(public timings: string | number,
public styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata) {
super();
}
}
export abstract class AnimationWithStepsMetadata extends AnimationMetadata {
constructor() { super(); }
get steps(): AnimationMetadata[] { throw new BaseException('NOT IMPLEMENTED: Base Class'); }
}
export class AnimationSequenceMetadata extends AnimationWithStepsMetadata {
constructor(private _steps: AnimationMetadata[]) { super(); }
get steps(): AnimationMetadata[] { return this._steps; }
}
export class AnimationGroupMetadata extends AnimationWithStepsMetadata {
constructor(private _steps: AnimationMetadata[]) { super(); }
get steps(): AnimationMetadata[] { return this._steps; }
}
export function animate(timing: string | number,
styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata = null): AnimationAnimateMetadata {
var stylesEntry = styles;
if (!isPresent(stylesEntry)) {
var EMPTY_STYLE: {[key: string]: string|number} = {};
stylesEntry = new AnimationStyleMetadata([EMPTY_STYLE], 1);
}
return new AnimationAnimateMetadata(timing, stylesEntry);
}
export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
return new AnimationGroupMetadata(steps);
}
export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata {
return new AnimationSequenceMetadata(steps);
}
export function style(tokens: string|{[key: string]: string | number}|Array<string|{[key: string]: string | number}>): AnimationStyleMetadata {
var input: Array<{[key: string]: string | number}|string>;
var offset: number = null;
if (isString(tokens)) {
input = [<string>tokens];
} else {
if (isArray(tokens)) {
input = <Array<{[key: string]: string | number}>>tokens;
} else {
input = [<{[key: string]: string | number}>tokens];
}
input.forEach(entry => {
var entryOffset = entry['offset'];
if (isPresent(entryOffset)) {
offset = offset == null ? NumberWrapper.parseFloat(entryOffset) : offset;
}
});
}
return new AnimationStyleMetadata(input, offset);
}
export function state(stateNameExpr: string, styles: AnimationStyleMetadata): AnimationStateDeclarationMetadata {
return new AnimationStateDeclarationMetadata(stateNameExpr, styles);
}
export function keyframes(steps: AnimationStyleMetadata|AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata {
var stepData = isArray(steps)
? <AnimationStyleMetadata[]>steps
: [<AnimationStyleMetadata>steps];
return new AnimationKeyframesSequenceMetadata(stepData);
}
export function transition(stateChangeExpr: string, animationData: AnimationMetadata|AnimationMetadata[]): AnimationStateTransitionMetadata {
var animation = isArray(animationData)
? new AnimationSequenceMetadata(<AnimationMetadata[]>animationData)
: <AnimationMetadata>animationData;
return new AnimationStateTransitionMetadata(stateChangeExpr, animation);
}
export function trigger(name: string, animation: AnimationMetadata|AnimationMetadata[]): AnimationEntryMetadata {
var entry = isArray(animation)
? <AnimationMetadata[]>animation
: [<AnimationMetadata>animation];
return new AnimationEntryMetadata(name, entry);
}