/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {AUTO_STYLE, AnimationMetadata, AnimationOptions, AnimationPlayer, ɵStyleData} from '@angular/animations'; import {Ast} from '../dsl/animation_ast'; import {buildAnimationAst} from '../dsl/animation_ast_builder'; import {buildAnimationTimelines} from '../dsl/animation_timeline_builder'; import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction'; import {ElementInstructionMap} from '../dsl/element_instruction_map'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; import {AnimationDriver} from './animation_driver'; import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared'; const EMPTY_INSTRUCTION_MAP = new ElementInstructionMap(); export class TimelineAnimationEngine { private _animations: {[id: string]: Ast} = {}; private _playersById: {[id: string]: AnimationPlayer} = {}; public players: AnimationPlayer[] = []; constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {} register(id: string, metadata: AnimationMetadata|AnimationMetadata[]) { const errors: any[] = []; const ast = buildAnimationAst(metadata, errors); if (errors.length) { throw new Error( `Unable to build the animation due to the following errors: ${errors.join("\n")}`); } else { this._animations[id] = ast; } } private _buildPlayer( i: AnimationTimelineInstruction, preStyles: ɵStyleData, postStyles?: ɵStyleData): AnimationPlayer { const element = i.element; const keyframes = normalizeKeyframes( this._driver, this._normalizer, element, i.keyframes, preStyles, postStyles); return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, []); } create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer { const errors: any[] = []; const ast = this._animations[id]; let instructions: AnimationTimelineInstruction[]; const autoStylesMap = new Map(); if (ast) { instructions = buildAnimationTimelines( this._driver, element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors); instructions.forEach(inst => { const styles = getOrSetAsInMap(autoStylesMap, inst.element, {}); inst.postStyleProps.forEach(prop => styles[prop] = null); }); } else { errors.push('The requested animation doesn\'t exist or has already been destroyed'); instructions = []; } if (errors.length) { throw new Error( `Unable to create the animation due to the following errors: ${errors.join("\n")}`); } autoStylesMap.forEach((styles, element) => { Object.keys(styles).forEach( prop => { styles[prop] = this._driver.computeStyle(element, prop, AUTO_STYLE); }); }); const players = instructions.map(i => { const styles = autoStylesMap.get(i.element); return this._buildPlayer(i, {}, styles); }); const player = optimizeGroupPlayer(players); this._playersById[id] = player; player.onDestroy(() => this.destroy(id)); this.players.push(player); return player; } destroy(id: string) { const player = this._getPlayer(id); player.destroy(); delete this._playersById[id]; const index = this.players.indexOf(player); if (index >= 0) { this.players.splice(index, 1); } } private _getPlayer(id: string): AnimationPlayer { const player = this._playersById[id]; if (!player) { throw new Error(`Unable to find the timeline player referenced by ${id}`); } return player; } listen(id: string, element: string, eventName: string, callback: (event: any) => any): () => void { // triggerName, fromState, toState are all ignored for timeline animations const baseEvent = makeAnimationEvent(element, '', '', ''); listenOnPlayer(this._getPlayer(id), eventName, baseEvent, callback); return () => {}; } command(id: string, element: any, command: string, args: any[]): void { if (command == 'register') { this.register(id, args[0] as AnimationMetadata | AnimationMetadata[]); return; } if (command == 'create') { const options = (args[0] || {}) as AnimationOptions; this.create(id, element, options); return; } const player = this._getPlayer(id); switch (command) { case 'play': player.play(); break; case 'pause': player.pause(); break; case 'reset': player.reset(); break; case 'restart': player.restart(); break; case 'finish': player.finish(); break; case 'init': player.init(); break; case 'setPosition': player.setPosition(parseFloat(args[0] as string)); break; case 'destroy': this.destroy(id); break; } } }