feat(animations): introduce a wave of new animation features
This commit is contained in:

committed by
Jason Aden

parent
d761059e4d
commit
16c8167886
@ -8,15 +8,19 @@
|
||||
import {AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
export abstract class AnimationEngine {
|
||||
abstract registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void;
|
||||
abstract onInsert(element: any, domFn: () => any): void;
|
||||
abstract onRemove(element: any, domFn: () => any): void;
|
||||
abstract setProperty(element: any, property: string, value: any): void;
|
||||
abstract registerTrigger(
|
||||
componentId: string, namespaceId: string, hostElement: any, name: string,
|
||||
metadata: AnimationTriggerMetadata): void;
|
||||
abstract onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void;
|
||||
abstract onRemove(namespaceId: string, element: any, context: any): void;
|
||||
abstract setProperty(namespaceId: string, element: any, property: string, value: any): void;
|
||||
abstract listen(
|
||||
element: any, eventName: string, eventPhase: string,
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any;
|
||||
abstract flush(): void;
|
||||
abstract destroy(namespaceId: string, context: any): void;
|
||||
|
||||
get activePlayers(): AnimationPlayer[] { throw new Error('...'); }
|
||||
get queuedPlayers(): AnimationPlayer[] { throw new Error('...'); }
|
||||
onRemovalComplete: (delegate: any, element: any) => void;
|
||||
|
||||
public players: AnimationPlayer[];
|
||||
}
|
||||
|
@ -5,23 +5,19 @@
|
||||
* 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 {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationDriver} from '../render/animation_driver';
|
||||
import {DomAnimationEngine} from '../render/dom_animation_engine';
|
||||
import {AnimationMetadata, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
import {normalizeStyles} from '../util';
|
||||
|
||||
import {Ast} from './animation_ast';
|
||||
import {buildAnimationAst} from './animation_ast_builder';
|
||||
import {buildAnimationTimelines} from './animation_timeline_builder';
|
||||
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
import {buildAnimationKeyframes} from './animation_timeline_visitor';
|
||||
import {validateAnimationSequence} from './animation_validator_visitor';
|
||||
import {AnimationStyleNormalizer} from './style_normalization/animation_style_normalizer';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
export class Animation {
|
||||
private _animationAst: AnimationMetadata;
|
||||
private _animationAst: Ast;
|
||||
constructor(input: AnimationMetadata|AnimationMetadata[]) {
|
||||
const ast =
|
||||
Array.isArray(input) ? sequence(<AnimationMetadata[]>input) : <AnimationMetadata>input;
|
||||
const errors = validateAnimationSequence(ast);
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(input, errors);
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation validation failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
@ -30,26 +26,21 @@ export class Animation {
|
||||
}
|
||||
|
||||
buildTimelines(
|
||||
startingStyles: ɵStyleData|ɵStyleData[],
|
||||
destinationStyles: ɵStyleData|ɵStyleData[]): AnimationTimelineInstruction[] {
|
||||
element: any, startingStyles: ɵStyleData|ɵStyleData[],
|
||||
destinationStyles: ɵStyleData|ɵStyleData[], options: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] {
|
||||
const start = Array.isArray(startingStyles) ? normalizeStyles(startingStyles) :
|
||||
<ɵStyleData>startingStyles;
|
||||
const dest = Array.isArray(destinationStyles) ? normalizeStyles(destinationStyles) :
|
||||
<ɵStyleData>destinationStyles;
|
||||
return buildAnimationKeyframes(this._animationAst, start, dest);
|
||||
}
|
||||
|
||||
// this is only used for development demo purposes for now
|
||||
private create(
|
||||
injector: any, element: any, startingStyles: ɵStyleData = {},
|
||||
destinationStyles: ɵStyleData = {}): AnimationPlayer {
|
||||
const instructions = this.buildTimelines(startingStyles, destinationStyles);
|
||||
|
||||
// note the code below is only here to make the tests happy (once the new renderer is
|
||||
// within core then the code below will interact with Renderer.transition(...))
|
||||
const driver: AnimationDriver = injector.get(AnimationDriver);
|
||||
const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer);
|
||||
const engine = new DomAnimationEngine(driver, normalizer);
|
||||
return engine.animateTimeline(element, instructions);
|
||||
const errors: any = [];
|
||||
subInstructions = subInstructions || new ElementInstructionMap();
|
||||
const result = buildAnimationTimelines(
|
||||
element, this._animationAst, start, dest, options, subInstructions, errors);
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
156
packages/animations/browser/src/dsl/animation_ast.ts
Normal file
156
packages/animations/browser/src/dsl/animation_ast.ts
Normal file
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @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
|
||||
/**
|
||||
* @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 {AnimateTimings, AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
const EMPTY_ANIMATION_OPTIONS: AnimationOptions = {};
|
||||
|
||||
export interface AstVisitor {
|
||||
visitTrigger(ast: TriggerAst, context: any): any;
|
||||
visitState(ast: StateAst, context: any): any;
|
||||
visitTransition(ast: TransitionAst, context: any): any;
|
||||
visitSequence(ast: SequenceAst, context: any): any;
|
||||
visitGroup(ast: GroupAst, context: any): any;
|
||||
visitAnimate(ast: AnimateAst, context: any): any;
|
||||
visitStyle(ast: StyleAst, context: any): any;
|
||||
visitKeyframes(ast: KeyframesAst, context: any): any;
|
||||
visitReference(ast: ReferenceAst, context: any): any;
|
||||
visitAnimateChild(ast: AnimateChildAst, context: any): any;
|
||||
visitAnimateRef(ast: AnimateRefAst, context: any): any;
|
||||
visitQuery(ast: QueryAst, context: any): any;
|
||||
visitStagger(ast: StaggerAst, context: any): any;
|
||||
visitTiming(ast: TimingAst, context: any): any;
|
||||
}
|
||||
|
||||
export abstract class Ast {
|
||||
abstract visit(ast: AstVisitor, context: any): any;
|
||||
public options: AnimationOptions = EMPTY_ANIMATION_OPTIONS;
|
||||
|
||||
get params(): {[name: string]: any}|null { return this.options['params'] || null; }
|
||||
}
|
||||
|
||||
export class TriggerAst extends Ast {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
|
||||
constructor(public name: string, public states: StateAst[], public transitions: TransitionAst[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTrigger(this, context); }
|
||||
}
|
||||
|
||||
export class StateAst extends Ast {
|
||||
constructor(public name: string, public style: StyleAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitState(this, context); }
|
||||
}
|
||||
|
||||
export class TransitionAst extends Ast {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
|
||||
constructor(
|
||||
public matchers: ((fromState: string, toState: string) => boolean)[], public animation: Ast) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTransition(this, context); }
|
||||
}
|
||||
|
||||
export class SequenceAst extends Ast {
|
||||
constructor(public steps: Ast[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitSequence(this, context); }
|
||||
}
|
||||
|
||||
export class GroupAst extends Ast {
|
||||
constructor(public steps: Ast[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitGroup(this, context); }
|
||||
}
|
||||
|
||||
export class AnimateAst extends Ast {
|
||||
constructor(public timings: TimingAst, public style: StyleAst|KeyframesAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimate(this, context); }
|
||||
}
|
||||
|
||||
export class StyleAst extends Ast {
|
||||
public isEmptyStep = false;
|
||||
|
||||
constructor(
|
||||
public styles: (ɵStyleData|string)[], public easing: string|null,
|
||||
public offset: number|null) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitStyle(this, context); }
|
||||
}
|
||||
|
||||
export class KeyframesAst extends Ast {
|
||||
constructor(public styles: StyleAst[]) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitKeyframes(this, context); }
|
||||
}
|
||||
|
||||
export class ReferenceAst extends Ast {
|
||||
constructor(public animation: Ast) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitReference(this, context); }
|
||||
}
|
||||
|
||||
export class AnimateChildAst extends Ast {
|
||||
constructor() { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateChild(this, context); }
|
||||
}
|
||||
|
||||
export class AnimateRefAst extends Ast {
|
||||
constructor(public animation: ReferenceAst) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateRef(this, context); }
|
||||
}
|
||||
|
||||
export class QueryAst extends Ast {
|
||||
public originalSelector: string;
|
||||
|
||||
constructor(
|
||||
public selector: string, public limit: number, public optional: boolean,
|
||||
public includeSelf: boolean, public animation: Ast) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitQuery(this, context); }
|
||||
}
|
||||
|
||||
export class StaggerAst extends Ast {
|
||||
constructor(public timings: AnimateTimings, public animation: Ast) { super(); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitStagger(this, context); }
|
||||
}
|
||||
|
||||
export class TimingAst extends Ast {
|
||||
constructor(
|
||||
public duration: number, public delay: number = 0, public easing: string|null = null) {
|
||||
super();
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
|
||||
}
|
||||
|
||||
export class DynamicTimingAst extends TimingAst {
|
||||
constructor(public value: string) { super(0, 0, ''); }
|
||||
|
||||
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
|
||||
}
|
475
packages/animations/browser/src/dsl/animation_ast_builder.ts
Normal file
475
packages/animations/browser/src/dsl/animation_ast_builder.ts
Normal file
@ -0,0 +1,475 @@
|
||||
/**
|
||||
* @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, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
|
||||
const SELF_TOKEN = ':self';
|
||||
const SELF_TOKEN_REGEX = new RegExp(`\s*${SELF_TOKEN}\s*,?`, 'g');
|
||||
|
||||
/*
|
||||
* [Validation]
|
||||
* The visitor code below will traverse the animation AST generated by the animation verb functions
|
||||
* (the output is a tree of objects) and attempt to perform a series of validations on the data. The
|
||||
* following corner-cases will be validated:
|
||||
*
|
||||
* 1. Overlap of animations
|
||||
* Given that a CSS property cannot be animated in more than one place at the same time, it's
|
||||
* important that this behaviour is detected and validated. The way in which this occurs is that
|
||||
* each time a style property is examined, a string-map containing the property will be updated with
|
||||
* the start and end times for when the property is used within an animation step.
|
||||
*
|
||||
* If there are two or more parallel animations that are currently running (these are invoked by the
|
||||
* group()) on the same element then the validator will throw an error. Since the start/end timing
|
||||
* values are collected for each property then if the current animation step is animating the same
|
||||
* property and its timing values fall anywhere into the window of time that the property is
|
||||
* currently being animated within then this is what causes an error.
|
||||
*
|
||||
* 2. Timing values
|
||||
* The validator will validate to see if a timing value of `duration delay easing` or
|
||||
* `durationNumber` is valid or not.
|
||||
*
|
||||
* (note that upon validation the code below will replace the timing data with an object containing
|
||||
* {duration,delay,easing}.
|
||||
*
|
||||
* 3. Offset Validation
|
||||
* Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
|
||||
* Offsets within keyframes() are considered valid when:
|
||||
*
|
||||
* - No offsets are used at all
|
||||
* - Each style() entry contains an offset value
|
||||
* - Each offset is between 0 and 1
|
||||
* - Each offset is greater to or equal than the previous one
|
||||
*
|
||||
* Otherwise an error will be thrown.
|
||||
*/
|
||||
export function buildAnimationAst(
|
||||
metadata: AnimationMetadata | AnimationMetadata[], errors: any[]): Ast {
|
||||
return new AnimationAstBuilderVisitor().build(metadata, errors);
|
||||
}
|
||||
|
||||
const LEAVE_TOKEN = ':leave';
|
||||
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
|
||||
const ENTER_TOKEN = ':enter';
|
||||
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
|
||||
const ROOT_SELECTOR = '';
|
||||
|
||||
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
|
||||
build(metadata: AnimationMetadata|AnimationMetadata[], errors: any[]): Ast {
|
||||
const context = new AnimationAstBuilderContext(errors);
|
||||
this._resetContextStyleTimingState(context);
|
||||
return visitAnimationNode(this, normalizeAnimationEntry(metadata), context) as Ast;
|
||||
}
|
||||
|
||||
private _resetContextStyleTimingState(context: AnimationAstBuilderContext) {
|
||||
context.currentQuerySelector = ROOT_SELECTOR;
|
||||
context.collectedStyles[ROOT_SELECTOR] = {};
|
||||
context.currentTime = 0;
|
||||
}
|
||||
|
||||
visitTrigger(metadata: AnimationTriggerMetadata, context: AnimationAstBuilderContext):
|
||||
TriggerAst {
|
||||
let queryCount = context.queryCount = 0;
|
||||
let depCount = context.depCount = 0;
|
||||
const states: StateAst[] = [];
|
||||
const transitions: TransitionAst[] = [];
|
||||
metadata.definitions.forEach(def => {
|
||||
this._resetContextStyleTimingState(context);
|
||||
if (def.type == AnimationMetadataType.State) {
|
||||
const stateDef = def as AnimationStateMetadata;
|
||||
const name = stateDef.name;
|
||||
name.split(/\s*,\s*/).forEach(n => {
|
||||
stateDef.name = n;
|
||||
states.push(this.visitState(stateDef, context));
|
||||
});
|
||||
stateDef.name = name;
|
||||
} else if (def.type == AnimationMetadataType.Transition) {
|
||||
const transition = this.visitTransition(def as AnimationTransitionMetadata, context);
|
||||
queryCount += transition.queryCount;
|
||||
depCount += transition.depCount;
|
||||
transitions.push(transition);
|
||||
} else {
|
||||
context.errors.push(
|
||||
'only state() and transition() definitions can sit inside of a trigger()');
|
||||
}
|
||||
});
|
||||
const ast = new TriggerAst(metadata.name, states, transitions);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
ast.queryCount = queryCount;
|
||||
ast.depCount = depCount;
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
|
||||
return new StateAst(metadata.name, this.visitStyle(metadata.styles, context));
|
||||
}
|
||||
|
||||
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
|
||||
TransitionAst {
|
||||
context.queryCount = 0;
|
||||
context.depCount = 0;
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const matchers = parseTransitionExpr(metadata.expr, context.errors);
|
||||
const ast = new TransitionAst(matchers, entry);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
ast.queryCount = context.queryCount;
|
||||
ast.depCount = context.depCount;
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitSequence(metadata: AnimationSequenceMetadata, context: AnimationAstBuilderContext):
|
||||
SequenceAst {
|
||||
const ast = new SequenceAst(metadata.steps.map(s => visitAnimationNode(this, s, context)));
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitGroup(metadata: AnimationGroupMetadata, context: AnimationAstBuilderContext): GroupAst {
|
||||
const currentTime = context.currentTime;
|
||||
let furthestTime = 0;
|
||||
const steps = metadata.steps.map(step => {
|
||||
context.currentTime = currentTime;
|
||||
const innerAst = visitAnimationNode(this, step, context);
|
||||
furthestTime = Math.max(furthestTime, context.currentTime);
|
||||
return innerAst;
|
||||
});
|
||||
|
||||
context.currentTime = furthestTime;
|
||||
const ast = new GroupAst(steps);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitAnimate(metadata: AnimationAnimateMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateAst {
|
||||
const timingAst = constructTimingAst(metadata.timings, context.errors);
|
||||
context.currentAnimateTimings = timingAst;
|
||||
|
||||
let styles: StyleAst|KeyframesAst;
|
||||
let styleMetadata: AnimationMetadata = metadata.styles ? metadata.styles : style({});
|
||||
if (styleMetadata.type == AnimationMetadataType.Keyframes) {
|
||||
styles = this.visitKeyframes(styleMetadata as AnimationKeyframesSequenceMetadata, context);
|
||||
} else {
|
||||
let styleMetadata = metadata.styles as AnimationStyleMetadata;
|
||||
let isEmpty = false;
|
||||
if (!styleMetadata) {
|
||||
isEmpty = true;
|
||||
const newStyleData: {[prop: string]: string | number} = {};
|
||||
if (timingAst.easing) {
|
||||
newStyleData['easing'] = timingAst.easing;
|
||||
}
|
||||
styleMetadata = style(newStyleData);
|
||||
}
|
||||
context.currentTime += timingAst.duration + timingAst.delay;
|
||||
const styleAst = this.visitStyle(styleMetadata, context);
|
||||
styleAst.isEmptyStep = isEmpty;
|
||||
styles = styleAst;
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
return new AnimateAst(timingAst, styles);
|
||||
}
|
||||
|
||||
visitStyle(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext): StyleAst {
|
||||
const ast = this._makeStyleAst(metadata, context);
|
||||
this._validateStyleAst(ast, context);
|
||||
return ast;
|
||||
}
|
||||
|
||||
private _makeStyleAst(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext):
|
||||
StyleAst {
|
||||
const styles: (ɵStyleData | string)[] = [];
|
||||
if (Array.isArray(metadata.styles)) {
|
||||
(metadata.styles as(ɵStyleData | string)[]).forEach(styleTuple => {
|
||||
if (typeof styleTuple == 'string') {
|
||||
if (styleTuple == AUTO_STYLE) {
|
||||
styles.push(styleTuple as string);
|
||||
} else {
|
||||
context.errors.push(`The provided style string value ${styleTuple} is not allowed.`);
|
||||
}
|
||||
} else {
|
||||
styles.push(styleTuple as ɵStyleData);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
styles.push(metadata.styles);
|
||||
}
|
||||
|
||||
let collectedEasing: string|null = null;
|
||||
styles.forEach(styleData => {
|
||||
if (isObject(styleData)) {
|
||||
const styleMap = styleData as ɵStyleData;
|
||||
const easing = styleMap['easing'];
|
||||
if (easing) {
|
||||
collectedEasing = easing as string;
|
||||
delete styleMap['easing'];
|
||||
}
|
||||
}
|
||||
});
|
||||
return new StyleAst(styles, collectedEasing, metadata.offset);
|
||||
}
|
||||
|
||||
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {
|
||||
const timings = context.currentAnimateTimings;
|
||||
let endTime = context.currentTime;
|
||||
let startTime = context.currentTime;
|
||||
if (timings && startTime > 0) {
|
||||
startTime -= timings.duration + timings.delay;
|
||||
}
|
||||
|
||||
ast.styles.forEach(tuple => {
|
||||
if (typeof tuple == 'string') return;
|
||||
|
||||
Object.keys(tuple).forEach(prop => {
|
||||
const collectedStyles = context.collectedStyles[context.currentQuerySelector !];
|
||||
const collectedEntry = collectedStyles[prop];
|
||||
let updateCollectedStyle = true;
|
||||
if (collectedEntry) {
|
||||
if (startTime != endTime && startTime >= collectedEntry.startTime &&
|
||||
endTime <= collectedEntry.endTime) {
|
||||
context.errors.push(
|
||||
`The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
|
||||
updateCollectedStyle = false;
|
||||
}
|
||||
|
||||
// we always choose the smaller start time value since we
|
||||
// want to have a record of the entire animation window where
|
||||
// the style property is being animated in between
|
||||
startTime = collectedEntry.startTime;
|
||||
}
|
||||
|
||||
if (updateCollectedStyle) {
|
||||
collectedStyles[prop] = {startTime, endTime};
|
||||
}
|
||||
|
||||
if (context.options) {
|
||||
validateStyleParams(tuple[prop], context.options, context.errors);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
visitKeyframes(metadata: AnimationKeyframesSequenceMetadata, context: AnimationAstBuilderContext):
|
||||
KeyframesAst {
|
||||
if (!context.currentAnimateTimings) {
|
||||
context.errors.push(`keyframes() must be placed inside of a call to animate()`);
|
||||
return new KeyframesAst([]);
|
||||
}
|
||||
|
||||
const MAX_KEYFRAME_OFFSET = 1;
|
||||
|
||||
let totalKeyframesWithOffsets = 0;
|
||||
const offsets: number[] = [];
|
||||
let offsetsOutOfOrder = false;
|
||||
let keyframesOutOfRange = false;
|
||||
let previousOffset: number = 0;
|
||||
|
||||
const keyframes: StyleAst[] = metadata.steps.map(styles => {
|
||||
const style = this._makeStyleAst(styles, context);
|
||||
let offsetVal: number|null =
|
||||
style.offset != null ? style.offset : consumeOffset(style.styles);
|
||||
let offset: number = 0;
|
||||
if (offsetVal != null) {
|
||||
totalKeyframesWithOffsets++;
|
||||
offset = style.offset = offsetVal;
|
||||
}
|
||||
keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
|
||||
offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
|
||||
previousOffset = offset;
|
||||
offsets.push(offset);
|
||||
return style;
|
||||
});
|
||||
|
||||
if (keyframesOutOfRange) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
|
||||
}
|
||||
|
||||
if (offsetsOutOfOrder) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are in order`);
|
||||
}
|
||||
|
||||
const length = metadata.steps.length;
|
||||
let generatedOffset = 0;
|
||||
if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
|
||||
context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
|
||||
} else if (totalKeyframesWithOffsets == 0) {
|
||||
generatedOffset = MAX_KEYFRAME_OFFSET / (length - 1);
|
||||
}
|
||||
|
||||
const limit = length - 1;
|
||||
const currentTime = context.currentTime;
|
||||
const currentAnimateTimings = context.currentAnimateTimings !;
|
||||
const animateDuration = currentAnimateTimings.duration;
|
||||
keyframes.forEach((kf, i) => {
|
||||
const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
|
||||
const durationUpToThisFrame = offset * animateDuration;
|
||||
context.currentTime = currentTime + currentAnimateTimings.delay + durationUpToThisFrame;
|
||||
currentAnimateTimings.duration = durationUpToThisFrame;
|
||||
this._validateStyleAst(kf, context);
|
||||
kf.offset = offset;
|
||||
});
|
||||
|
||||
return new KeyframesAst(keyframes);
|
||||
}
|
||||
|
||||
visitReference(metadata: AnimationReferenceMetadata, context: AnimationAstBuilderContext):
|
||||
ReferenceAst {
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
const ast = new ReferenceAst(entry);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitAnimateChild(metadata: AnimationAnimateChildMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateChildAst {
|
||||
context.depCount++;
|
||||
const ast = new AnimateChildAst();
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitAnimateRef(metadata: AnimationAnimateRefMetadata, context: AnimationAstBuilderContext):
|
||||
AnimateRefAst {
|
||||
const animation = this.visitReference(metadata.animation, context);
|
||||
const ast = new AnimateRefAst(animation);
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitQuery(metadata: AnimationQueryMetadata, context: AnimationAstBuilderContext): QueryAst {
|
||||
const parentSelector = context.currentQuerySelector !;
|
||||
const options = (metadata.options || {}) as AnimationQueryOptions;
|
||||
|
||||
context.queryCount++;
|
||||
context.currentQuery = metadata;
|
||||
const [selector, includeSelf] = normalizeSelector(metadata.selector);
|
||||
context.currentQuerySelector =
|
||||
parentSelector.length ? (parentSelector + ' ' + selector) : selector;
|
||||
getOrSetAsInMap(context.collectedStyles, context.currentQuerySelector, {});
|
||||
|
||||
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
context.currentQuery = null;
|
||||
context.currentQuerySelector = parentSelector;
|
||||
|
||||
const ast = new QueryAst(selector, options.limit || 0, !!options.optional, includeSelf, entry);
|
||||
ast.originalSelector = metadata.selector;
|
||||
ast.options = normalizeAnimationOptions(metadata.options);
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitStagger(metadata: AnimationStaggerMetadata, context: AnimationAstBuilderContext):
|
||||
StaggerAst {
|
||||
if (!context.currentQuery) {
|
||||
context.errors.push(`stagger() can only be used inside of query()`);
|
||||
}
|
||||
const timings = metadata.timings === 'full' ?
|
||||
{duration: 0, delay: 0, easing: 'full'} :
|
||||
resolveTiming(metadata.timings, context.errors, true);
|
||||
const animation =
|
||||
visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
|
||||
return new StaggerAst(timings, animation);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSelector(selector: string): [string, boolean] {
|
||||
const hasAmpersand = selector.split(/\s*,\s*/).find(token => token == SELF_TOKEN) ? true : false;
|
||||
if (hasAmpersand) {
|
||||
selector = selector.replace(SELF_TOKEN_REGEX, '');
|
||||
}
|
||||
|
||||
selector = selector.replace(ENTER_TOKEN_REGEX, ENTER_SELECTOR)
|
||||
.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR)
|
||||
.replace(/@\*/g, NG_TRIGGER_SELECTOR)
|
||||
.replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.substr(1))
|
||||
.replace(/:animating/g, NG_ANIMATING_SELECTOR);
|
||||
|
||||
return [selector, hasAmpersand];
|
||||
}
|
||||
|
||||
|
||||
function normalizeParams(obj: {[key: string]: any} | any): {[key: string]: any}|null {
|
||||
return obj ? copyObj(obj) : null;
|
||||
}
|
||||
|
||||
export type StyleTimeTuple = {
|
||||
startTime: number; endTime: number;
|
||||
};
|
||||
|
||||
export class AnimationAstBuilderContext {
|
||||
public queryCount: number = 0;
|
||||
public depCount: number = 0;
|
||||
public currentTransition: AnimationTransitionMetadata|null = null;
|
||||
public currentQuery: AnimationQueryMetadata|null = null;
|
||||
public currentQuerySelector: string|null = null;
|
||||
public currentAnimateTimings: TimingAst|null = null;
|
||||
public currentTime: number = 0;
|
||||
public collectedStyles: {[selectorName: string]: {[propName: string]: StyleTimeTuple}} = {};
|
||||
public options: AnimationOptions|null = null;
|
||||
constructor(public errors: any[]) {}
|
||||
}
|
||||
|
||||
function consumeOffset(styles: ɵStyleData | string | (ɵStyleData | string)[]): number|null {
|
||||
if (typeof styles == 'string') return null;
|
||||
|
||||
let offset: number|null = null;
|
||||
|
||||
if (Array.isArray(styles)) {
|
||||
styles.forEach(styleTuple => {
|
||||
if (isObject(styleTuple) && styleTuple.hasOwnProperty('offset')) {
|
||||
const obj = styleTuple as ɵStyleData;
|
||||
offset = parseFloat(obj['offset'] as string);
|
||||
delete obj['offset'];
|
||||
}
|
||||
});
|
||||
} else if (isObject(styles) && styles.hasOwnProperty('offset')) {
|
||||
const obj = styles as ɵStyleData;
|
||||
offset = parseFloat(obj['offset'] as string);
|
||||
delete obj['offset'];
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
function isObject(value: any): boolean {
|
||||
return !Array.isArray(value) && typeof value == 'object';
|
||||
}
|
||||
|
||||
function constructTimingAst(value: string | number | AnimateTimings, errors: any[]) {
|
||||
let timings: AnimateTimings|null = null;
|
||||
if (value.hasOwnProperty('duration')) {
|
||||
timings = value as AnimateTimings;
|
||||
} else if (typeof value == 'number') {
|
||||
const duration = resolveTiming(value as number, errors).duration;
|
||||
return new TimingAst(value as number, 0, '');
|
||||
}
|
||||
|
||||
const strValue = value as string;
|
||||
const isDynamic = strValue.split(/\s+/).some(v => v.charAt(0) == '{' && v.charAt(1) == '{');
|
||||
if (isDynamic) {
|
||||
return new DynamicTimingAst(strValue);
|
||||
}
|
||||
|
||||
timings = timings || resolveTiming(strValue, errors);
|
||||
return new TimingAst(timings.duration, timings.delay, timings.easing);
|
||||
}
|
||||
|
||||
function normalizeAnimationOptions(options: AnimationOptions | null): AnimationOptions {
|
||||
if (options) {
|
||||
options = copyObj(options);
|
||||
if (options['params']) {
|
||||
options['params'] = normalizeParams(options['params']) !;
|
||||
}
|
||||
} else {
|
||||
options = {};
|
||||
}
|
||||
return options;
|
||||
}
|
@ -5,35 +5,53 @@
|
||||
* 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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations';
|
||||
import {AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationQueryMetadata, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
export interface AnimationDslVisitor {
|
||||
visitTrigger(ast: AnimationTriggerMetadata, context: any): any;
|
||||
visitState(ast: AnimationStateMetadata, context: any): any;
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any;
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: any): any;
|
||||
visitGroup(ast: AnimationGroupMetadata, context: any): any;
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: any): any;
|
||||
visitStyle(ast: AnimationStyleMetadata, context: any): any;
|
||||
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any): any;
|
||||
visitKeyframes(ast: AnimationKeyframesSequenceMetadata, context: any): any;
|
||||
visitReference(ast: AnimationReferenceMetadata, context: any): any;
|
||||
visitAnimateChild(ast: AnimationAnimateChildMetadata, context: any): any;
|
||||
visitAnimateRef(ast: AnimationAnimateRefMetadata, context: any): any;
|
||||
visitQuery(ast: AnimationQueryMetadata, context: any): any;
|
||||
visitStagger(ast: AnimationStaggerMetadata, context: any): any;
|
||||
}
|
||||
|
||||
export function visitAnimationNode(
|
||||
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any) {
|
||||
switch (node.type) {
|
||||
case AnimationMetadataType.Trigger:
|
||||
return visitor.visitTrigger(node as AnimationTriggerMetadata, context);
|
||||
case AnimationMetadataType.State:
|
||||
return visitor.visitState(<AnimationStateMetadata>node, context);
|
||||
return visitor.visitState(node as AnimationStateMetadata, context);
|
||||
case AnimationMetadataType.Transition:
|
||||
return visitor.visitTransition(<AnimationTransitionMetadata>node, context);
|
||||
return visitor.visitTransition(node as AnimationTransitionMetadata, context);
|
||||
case AnimationMetadataType.Sequence:
|
||||
return visitor.visitSequence(<AnimationSequenceMetadata>node, context);
|
||||
return visitor.visitSequence(node as AnimationSequenceMetadata, context);
|
||||
case AnimationMetadataType.Group:
|
||||
return visitor.visitGroup(<AnimationGroupMetadata>node, context);
|
||||
return visitor.visitGroup(node as AnimationGroupMetadata, context);
|
||||
case AnimationMetadataType.Animate:
|
||||
return visitor.visitAnimate(<AnimationAnimateMetadata>node, context);
|
||||
case AnimationMetadataType.KeyframeSequence:
|
||||
return visitor.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>node, context);
|
||||
return visitor.visitAnimate(node as AnimationAnimateMetadata, context);
|
||||
case AnimationMetadataType.Keyframes:
|
||||
return visitor.visitKeyframes(node as AnimationKeyframesSequenceMetadata, context);
|
||||
case AnimationMetadataType.Style:
|
||||
return visitor.visitStyle(<AnimationStyleMetadata>node, context);
|
||||
return visitor.visitStyle(node as AnimationStyleMetadata, context);
|
||||
case AnimationMetadataType.Reference:
|
||||
return visitor.visitReference(node as AnimationReferenceMetadata, context);
|
||||
case AnimationMetadataType.AnimateChild:
|
||||
return visitor.visitAnimateChild(node as AnimationAnimateChildMetadata, context);
|
||||
case AnimationMetadataType.AnimateRef:
|
||||
return visitor.visitAnimateRef(node as AnimationAnimateRefMetadata, context);
|
||||
case AnimationMetadataType.Query:
|
||||
return visitor.visitQuery(node as AnimationQueryMetadata, context);
|
||||
case AnimationMetadataType.Stagger:
|
||||
return visitor.visitStagger(node as AnimationStaggerMetadata, context);
|
||||
default:
|
||||
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
|
||||
}
|
||||
|
@ -0,0 +1,873 @@
|
||||
/**
|
||||
* @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, AnimateTimings, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyObj, copyStyles, interpolateParams, iteratorToArray, resolveTiming, resolveTimingValue} from '../util';
|
||||
|
||||
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, AstVisitor, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
const ONE_FRAME_IN_MILLISECONDS = 1;
|
||||
|
||||
/*
|
||||
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
||||
* animation DSL code.
|
||||
*
|
||||
* The code below will be converted from:
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ opacity: 0 }),
|
||||
* animate(1000, style({ opacity: 0 }))
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* To:
|
||||
* ```
|
||||
* keyframes = [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }]
|
||||
* duration = 1000
|
||||
* delay = 0
|
||||
* easing = ''
|
||||
* ```
|
||||
*
|
||||
* For this operation to cover the combination of animation verbs (style, animate, group, etc...) a
|
||||
* combination of prototypical inheritance, AST traversal and merge-sort-like algorithms are used.
|
||||
*
|
||||
* [AST Traversal]
|
||||
* Each of the animation verbs, when executed, will return an string-map object representing what
|
||||
* type of action it is (style, animate, group, etc...) and the data associated with it. This means
|
||||
* that when functional composition mix of these functions is evaluated (like in the example above)
|
||||
* then it will end up producing a tree of objects representing the animation itself.
|
||||
*
|
||||
* When this animation object tree is processed by the visitor code below it will visit each of the
|
||||
* verb statements within the visitor. And during each visit it will build the context of the
|
||||
* animation keyframes by interacting with the `TimelineBuilder`.
|
||||
*
|
||||
* [TimelineBuilder]
|
||||
* This class is responsible for tracking the styles and building a series of keyframe objects for a
|
||||
* timeline between a start and end time. The builder starts off with an initial timeline and each
|
||||
* time the AST comes across a `group()`, `keyframes()` or a combination of the two wihtin a
|
||||
* `sequence()` then it will generate a sub timeline for each step as well as a new one after
|
||||
* they are complete.
|
||||
*
|
||||
* As the AST is traversed, the timing state on each of the timelines will be incremented. If a sub
|
||||
* timeline was created (based on one of the cases above) then the parent timeline will attempt to
|
||||
* merge the styles used within the sub timelines into itself (only with group() this will happen).
|
||||
* This happens with a merge operation (much like how the merge works in mergesort) and it will only
|
||||
* copy the most recently used styles from the sub timelines into the parent timeline. This ensures
|
||||
* that if the styles are used later on in another phase of the animation then they will be the most
|
||||
* up-to-date values.
|
||||
*
|
||||
* [How Missing Styles Are Updated]
|
||||
* Each timeline has a `backFill` property which is responsible for filling in new styles into
|
||||
* already processed keyframes if a new style shows up later within the animation sequence.
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ width: 0 }),
|
||||
* animate(1000, style({ width: 100 })),
|
||||
* animate(1000, style({ width: 200 })),
|
||||
* animate(1000, style({ width: 300 }))
|
||||
* animate(1000, style({ width: 400, height: 400 })) // notice how `height` doesn't exist anywhere
|
||||
* else
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* What is happening here is that the `height` value is added later in the sequence, but is missing
|
||||
* from all previous animation steps. Therefore when a keyframe is created it would also be missing
|
||||
* from all previous keyframes up until where it is first used. For the timeline keyframe generation
|
||||
* to properly fill in the style it will place the previous value (the value from the parent
|
||||
* timeline) or a default value of `*` into the backFill object. Given that each of the keyframe
|
||||
* styles are objects that prototypically inhert from the backFill object, this means that if a
|
||||
* value is added into the backFill then it will automatically propagate any missing values to all
|
||||
* keyframes. Therefore the missing `height` value will be properly filled into the already
|
||||
* processed keyframes.
|
||||
*
|
||||
* When a sub-timeline is created it will have its own backFill property. This is done so that
|
||||
* styles present within the sub-timeline do not accidentally seep into the previous/future timeline
|
||||
* keyframes
|
||||
*
|
||||
* (For prototypically-inherited contents to be detected a `for(i in obj)` loop must be used.)
|
||||
*
|
||||
* [Validation]
|
||||
* The code in this file is not responsible for validation. That functionality happens with within
|
||||
* the `AnimationValidatorVisitor` code.
|
||||
*/
|
||||
export function buildAnimationTimelines(
|
||||
rootElement: any, ast: Ast, startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {},
|
||||
options: AnimationOptions, subInstructions?: ElementInstructionMap,
|
||||
errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
return new AnimationTimelineBuilderVisitor().buildKeyframes(
|
||||
rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
|
||||
}
|
||||
|
||||
export declare type StyleAtTime = {
|
||||
time: number; value: string | number;
|
||||
};
|
||||
|
||||
const DEFAULT_NOOP_PREVIOUS_NODE = <Ast>{};
|
||||
export class AnimationTimelineContext {
|
||||
public parentContext: AnimationTimelineContext|null = null;
|
||||
public currentTimeline: TimelineBuilder;
|
||||
public currentAnimateTimings: AnimateTimings|null = null;
|
||||
public previousNode: Ast = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
public subContextCount = 0;
|
||||
public options: AnimationOptions = {};
|
||||
public currentQueryIndex: number = 0;
|
||||
public currentQueryTotal: number = 0;
|
||||
public currentStaggerTime: number = 0;
|
||||
|
||||
constructor(
|
||||
public element: any, public subInstructions: ElementInstructionMap, public errors: any[],
|
||||
public timelines: TimelineBuilder[], initialTimeline?: TimelineBuilder) {
|
||||
this.currentTimeline = initialTimeline || new TimelineBuilder(element, 0);
|
||||
timelines.push(this.currentTimeline);
|
||||
}
|
||||
|
||||
get params() { return this.options.params; }
|
||||
|
||||
updateOptions(newOptions: AnimationOptions|null, skipIfExists?: boolean) {
|
||||
if (!newOptions) return;
|
||||
|
||||
if (newOptions.duration != null) {
|
||||
this.options.duration = resolveTimingValue(newOptions.duration);
|
||||
}
|
||||
|
||||
if (newOptions.delay != null) {
|
||||
this.options.delay = resolveTimingValue(newOptions.delay);
|
||||
}
|
||||
|
||||
const newParams = newOptions.params;
|
||||
if (newParams) {
|
||||
let params: {[name: string]: any} = this.options && this.options.params !;
|
||||
if (!params) {
|
||||
params = this.options.params = {};
|
||||
}
|
||||
|
||||
Object.keys(params).forEach(name => {
|
||||
const value = params[name];
|
||||
if (!skipIfExists || !newOptions.hasOwnProperty(name)) {
|
||||
params[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _copyOptions() {
|
||||
const options: AnimationOptions = {};
|
||||
if (this.options) {
|
||||
const oldParams = this.options.params;
|
||||
if (oldParams) {
|
||||
const params: {[name: string]: any} = options['params'] = {};
|
||||
Object.keys(this.options.params).forEach(name => { params[name] = oldParams[name]; });
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
createSubContext(options: AnimationOptions|null = null, element?: any, newTime?: number):
|
||||
AnimationTimelineContext {
|
||||
const target = element || this.element;
|
||||
const context = new AnimationTimelineContext(
|
||||
target, this.subInstructions, this.errors, this.timelines,
|
||||
this.currentTimeline.fork(target, newTime || 0));
|
||||
context.previousNode = this.previousNode;
|
||||
context.currentAnimateTimings = this.currentAnimateTimings;
|
||||
|
||||
context.options = this._copyOptions();
|
||||
context.updateOptions(options);
|
||||
|
||||
context.currentQueryIndex = this.currentQueryIndex;
|
||||
context.currentQueryTotal = this.currentQueryTotal;
|
||||
context.parentContext = this;
|
||||
this.subContextCount++;
|
||||
return context;
|
||||
}
|
||||
|
||||
transformIntoNewTimeline(newTime?: number) {
|
||||
this.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
this.currentTimeline = this.currentTimeline.fork(this.element, newTime);
|
||||
this.timelines.push(this.currentTimeline);
|
||||
return this.currentTimeline;
|
||||
}
|
||||
|
||||
appendInstructionToTimeline(
|
||||
instruction: AnimationTimelineInstruction, duration: number|null,
|
||||
delay: number|null): AnimateTimings {
|
||||
const updatedTimings: AnimateTimings = {
|
||||
duration: duration != null ? duration : instruction.duration,
|
||||
delay: this.currentTimeline.currentTime + (delay != null ? delay : 0) + instruction.delay,
|
||||
easing: ''
|
||||
};
|
||||
const builder = new SubTimelineBuilder(
|
||||
instruction.element, instruction.keyframes, instruction.preStyleProps,
|
||||
instruction.postStyleProps, updatedTimings, instruction.stretchStartingKeyframe);
|
||||
this.timelines.push(builder);
|
||||
return updatedTimings;
|
||||
}
|
||||
|
||||
incrementTime(time: number) {
|
||||
this.currentTimeline.forwardTime(this.currentTimeline.duration + time);
|
||||
}
|
||||
|
||||
delayNextStep(delay: number) {
|
||||
// negative delays are not yet supported
|
||||
if (delay > 0) {
|
||||
this.currentTimeline.delayNextStep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationTimelineBuilderVisitor implements AstVisitor {
|
||||
buildKeyframes(
|
||||
rootElement: any, ast: Ast, startingStyles: ɵStyleData, finalStyles: ɵStyleData,
|
||||
options: AnimationOptions, subInstructions?: ElementInstructionMap,
|
||||
errors: any[] = []): AnimationTimelineInstruction[] {
|
||||
subInstructions = subInstructions || new ElementInstructionMap();
|
||||
const context = new AnimationTimelineContext(rootElement, subInstructions, errors, []);
|
||||
context.options = options;
|
||||
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
|
||||
|
||||
ast.visit(this, context);
|
||||
|
||||
// this checks to see if an actual animation happened
|
||||
const timelines = context.timelines.filter(timeline => timeline.containsAnimation());
|
||||
if (timelines.length && Object.keys(finalStyles).length) {
|
||||
const tl = timelines[timelines.length - 1];
|
||||
if (!tl.allowOnlyTimelineStyles()) {
|
||||
tl.setStyles([finalStyles], null, context.errors, options);
|
||||
}
|
||||
}
|
||||
|
||||
return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) :
|
||||
[createTimelineInstruction(rootElement, [], [], [], 0, 0, '', false)];
|
||||
}
|
||||
|
||||
visitTrigger(ast: TriggerAst, context: AnimationTimelineContext): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitState(ast: StateAst, context: AnimationTimelineContext): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitTransition(ast: TransitionAst, context: AnimationTimelineContext): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitAnimateChild(ast: AnimateChildAst, context: AnimationTimelineContext): any {
|
||||
const elementInstructions = context.subInstructions.consume(context.element);
|
||||
if (elementInstructions) {
|
||||
const innerContext = context.createSubContext(ast.options);
|
||||
const startTime = context.currentTimeline.currentTime;
|
||||
const endTime = this._visitSubInstructions(elementInstructions, innerContext);
|
||||
if (startTime != endTime) {
|
||||
// we do this on the upper context because we created a sub context for
|
||||
// the sub child animations
|
||||
context.transformIntoNewTimeline(endTime);
|
||||
}
|
||||
}
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitAnimateRef(ast: AnimateRefAst, context: AnimationTimelineContext): any {
|
||||
const innerContext = context.createSubContext(ast.options);
|
||||
innerContext.transformIntoNewTimeline();
|
||||
this.visitReference(ast.animation, innerContext);
|
||||
context.transformIntoNewTimeline(innerContext.currentTimeline.currentTime);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
private _visitSubInstructions(
|
||||
instructions: AnimationTimelineInstruction[], context: AnimationTimelineContext): number {
|
||||
const options = context.options;
|
||||
const startTime = context.currentTimeline.currentTime;
|
||||
let furthestTime = startTime;
|
||||
|
||||
// this is a special-case for when a user wants to skip a sub
|
||||
// animation from being fired entirely.
|
||||
const duration = options.duration != null ? resolveTimingValue(options.duration) : null;
|
||||
const delay = options.delay != null ? resolveTimingValue(options.delay) : null;
|
||||
if (duration !== 0) {
|
||||
instructions.forEach(instruction => {
|
||||
const instructionTimings =
|
||||
context.appendInstructionToTimeline(instruction, duration, delay);
|
||||
furthestTime =
|
||||
Math.max(furthestTime, instructionTimings.duration + instructionTimings.delay);
|
||||
});
|
||||
}
|
||||
|
||||
return furthestTime;
|
||||
}
|
||||
|
||||
visitReference(ast: ReferenceAst, context: AnimationTimelineContext) {
|
||||
context.updateOptions(ast.options, true);
|
||||
ast.animation.visit(this, context);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitSequence(ast: SequenceAst, context: AnimationTimelineContext) {
|
||||
const subContextCount = context.subContextCount;
|
||||
const options = ast.options;
|
||||
|
||||
if (options && (options.params || options.delay)) {
|
||||
context.createSubContext(options);
|
||||
context.transformIntoNewTimeline();
|
||||
|
||||
if (options.delay != null) {
|
||||
if (context.previousNode instanceof StyleAst) {
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
}
|
||||
|
||||
const delay = resolveTimingValue(options.delay);
|
||||
context.delayNextStep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.steps.length) {
|
||||
ast.steps.forEach(s => s.visit(this, context));
|
||||
|
||||
// this is here just incase the inner steps only contain or end with a style() call
|
||||
context.currentTimeline.applyStylesToKeyframe();
|
||||
|
||||
// this means that some animation function within the sequence
|
||||
// ended up creating a sub timeline (which means the current
|
||||
// timeline cannot overlap with the contents of the sequence)
|
||||
if (context.subContextCount > subContextCount) {
|
||||
context.transformIntoNewTimeline();
|
||||
}
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitGroup(ast: GroupAst, context: AnimationTimelineContext) {
|
||||
const innerTimelines: TimelineBuilder[] = [];
|
||||
let furthestTime = context.currentTimeline.currentTime;
|
||||
const delay = ast.options && ast.options.delay ? resolveTimingValue(ast.options.delay) : 0;
|
||||
|
||||
ast.steps.forEach(s => {
|
||||
const innerContext = context.createSubContext(ast.options);
|
||||
if (delay) {
|
||||
innerContext.delayNextStep(delay);
|
||||
}
|
||||
|
||||
s.visit(this, innerContext);
|
||||
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
|
||||
innerTimelines.push(innerContext.currentTimeline);
|
||||
});
|
||||
|
||||
// this operation is run after the AST loop because otherwise
|
||||
// if the parent timeline's collected styles were updated then
|
||||
// it would pass in invalid data into the new-to-be forked items
|
||||
innerTimelines.forEach(
|
||||
timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline));
|
||||
context.transformIntoNewTimeline(furthestTime);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitTiming(ast: TimingAst, context: AnimationTimelineContext): AnimateTimings {
|
||||
if (ast instanceof DynamicTimingAst) {
|
||||
const strValue = context.params ?
|
||||
interpolateParams(ast.value, context.params, context.errors) :
|
||||
ast.value.toString();
|
||||
return resolveTiming(strValue, context.errors);
|
||||
} else {
|
||||
return {duration: ast.duration, delay: ast.delay, easing: ast.easing};
|
||||
}
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimateAst, context: AnimationTimelineContext) {
|
||||
const timings = context.currentAnimateTimings = this.visitTiming(ast.timings, context);
|
||||
const timeline = context.currentTimeline;
|
||||
if (timings.delay) {
|
||||
context.incrementTime(timings.delay);
|
||||
timeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
const style = ast.style;
|
||||
if (style instanceof KeyframesAst) {
|
||||
this.visitKeyframes(style, context);
|
||||
} else {
|
||||
context.incrementTime(timings.duration);
|
||||
this.visitStyle(style as StyleAst, context);
|
||||
timeline.applyStylesToKeyframe();
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitStyle(ast: StyleAst, context: AnimationTimelineContext) {
|
||||
const timeline = context.currentTimeline;
|
||||
const timings = context.currentAnimateTimings !;
|
||||
|
||||
// this is a special case for when a style() call
|
||||
// directly follows an animate() call (but not inside of an animate() call)
|
||||
if (!timings && timeline.getCurrentStyleProperties().length) {
|
||||
timeline.forwardFrame();
|
||||
}
|
||||
|
||||
const easing = (timings && timings.easing) || ast.easing;
|
||||
if (ast.isEmptyStep) {
|
||||
timeline.applyEmptyStep(easing);
|
||||
} else {
|
||||
timeline.setStyles(ast.styles, easing, context.errors, context.options);
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitKeyframes(ast: KeyframesAst, context: AnimationTimelineContext) {
|
||||
const currentAnimateTimings = context.currentAnimateTimings !;
|
||||
const startTime = (context.currentTimeline !).duration;
|
||||
const duration = currentAnimateTimings.duration;
|
||||
const innerContext = context.createSubContext();
|
||||
const innerTimeline = innerContext.currentTimeline;
|
||||
innerTimeline.easing = currentAnimateTimings.easing;
|
||||
|
||||
ast.styles.forEach(step => {
|
||||
const offset: number = step.offset || 0;
|
||||
innerTimeline.forwardTime(offset * duration);
|
||||
innerTimeline.setStyles(step.styles, step.easing, context.errors, context.options);
|
||||
innerTimeline.applyStylesToKeyframe();
|
||||
});
|
||||
|
||||
// this will ensure that the parent timeline gets all the styles from
|
||||
// the child even if the new timeline below is not used
|
||||
context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline);
|
||||
|
||||
// we do this because the window between this timeline and the sub timeline
|
||||
// should ensure that the styles within are exactly the same as they were before
|
||||
context.transformIntoNewTimeline(startTime + duration);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitQuery(ast: QueryAst, context: AnimationTimelineContext) {
|
||||
// in the event that the first step before this is a style step we need
|
||||
// to ensure the styles are applied before the children are animated
|
||||
const startTime = context.currentTimeline.currentTime;
|
||||
const options = (ast.options || {}) as AnimationQueryOptions;
|
||||
const delay = options.delay ? resolveTimingValue(options.delay) : 0;
|
||||
|
||||
if (delay && (context.previousNode instanceof StyleAst ||
|
||||
(startTime == 0 && context.currentTimeline.getCurrentStyleProperties().length))) {
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
|
||||
}
|
||||
|
||||
let furthestTime = startTime;
|
||||
const elms = invokeQuery(
|
||||
context.element, ast.selector, ast.originalSelector, ast.limit, ast.includeSelf,
|
||||
options.optional ? true : false, context.errors);
|
||||
|
||||
context.currentQueryTotal = elms.length;
|
||||
let sameElementTimeline: TimelineBuilder|null = null;
|
||||
elms.forEach((element, i) => {
|
||||
|
||||
context.currentQueryIndex = i;
|
||||
const innerContext = context.createSubContext(ast.options, element);
|
||||
if (delay) {
|
||||
innerContext.delayNextStep(delay);
|
||||
}
|
||||
|
||||
if (element === context.element) {
|
||||
sameElementTimeline = innerContext.currentTimeline;
|
||||
}
|
||||
|
||||
ast.animation.visit(this, innerContext);
|
||||
|
||||
// this is here just incase the inner steps only contain or end
|
||||
// with a style() call (which is here to signal that this is a preparatory
|
||||
// call to style an element before it is animated again)
|
||||
innerContext.currentTimeline.applyStylesToKeyframe();
|
||||
|
||||
const endTime = innerContext.currentTimeline.currentTime;
|
||||
furthestTime = Math.max(furthestTime, endTime);
|
||||
});
|
||||
|
||||
context.currentQueryIndex = 0;
|
||||
context.currentQueryTotal = 0;
|
||||
context.transformIntoNewTimeline(furthestTime);
|
||||
|
||||
if (sameElementTimeline) {
|
||||
context.currentTimeline.mergeTimelineCollectedStyles(sameElementTimeline);
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitStagger(ast: StaggerAst, context: AnimationTimelineContext) {
|
||||
const parentContext = context.parentContext !;
|
||||
const tl = context.currentTimeline;
|
||||
const timings = ast.timings;
|
||||
const duration = Math.abs(timings.duration);
|
||||
const maxTime = duration * (context.currentQueryTotal - 1);
|
||||
let delay = duration * context.currentQueryIndex;
|
||||
|
||||
let staggerTransformer = timings.duration < 0 ? 'reverse' : timings.easing;
|
||||
switch (staggerTransformer) {
|
||||
case 'reverse':
|
||||
delay = maxTime - delay;
|
||||
break;
|
||||
case 'full':
|
||||
delay = parentContext.currentStaggerTime;
|
||||
break;
|
||||
}
|
||||
|
||||
if (delay) {
|
||||
context.currentTimeline.delayNextStep(delay);
|
||||
}
|
||||
|
||||
const startingTime = context.currentTimeline.currentTime;
|
||||
ast.animation.visit(this, context);
|
||||
context.previousNode = ast;
|
||||
|
||||
// time = duration + delay
|
||||
// the reason why this computation is so complex is because
|
||||
// the inner timeline may either have a delay value or a stretched
|
||||
// keyframe depending on if a subtimeline is not used or is used.
|
||||
parentContext.currentStaggerTime =
|
||||
(tl.currentTime - startingTime) + (tl.startTime - parentContext.currentTimeline.startTime);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimelineBuilder {
|
||||
public duration: number = 0;
|
||||
public easing: string|null;
|
||||
private _previousKeyframe: ɵStyleData = {};
|
||||
private _currentKeyframe: ɵStyleData = {};
|
||||
private _keyframes = new Map<number, ɵStyleData>();
|
||||
private _styleSummary: {[prop: string]: StyleAtTime} = {};
|
||||
private _localTimelineStyles: ɵStyleData;
|
||||
private _globalTimelineStyles: ɵStyleData;
|
||||
private _pendingStyles: ɵStyleData = {};
|
||||
private _backFill: ɵStyleData = {};
|
||||
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
|
||||
|
||||
constructor(
|
||||
public element: any, public startTime: number,
|
||||
private _elementTimelineStylesLookup?: Map<any, ɵStyleData>) {
|
||||
if (!this._elementTimelineStylesLookup) {
|
||||
this._elementTimelineStylesLookup = new Map<any, ɵStyleData>();
|
||||
}
|
||||
|
||||
this._localTimelineStyles = Object.create(this._backFill, {});
|
||||
this._globalTimelineStyles = this._elementTimelineStylesLookup.get(element) !;
|
||||
if (!this._globalTimelineStyles) {
|
||||
this._globalTimelineStyles = this._localTimelineStyles;
|
||||
this._elementTimelineStylesLookup.set(element, this._localTimelineStyles);
|
||||
}
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
containsAnimation(): boolean {
|
||||
switch (this._keyframes.size) {
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
return this.getCurrentStyleProperties().length > 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentStyleProperties(): string[] { return Object.keys(this._currentKeyframe); }
|
||||
|
||||
get currentTime() { return this.startTime + this.duration; }
|
||||
|
||||
delayNextStep(delay: number) {
|
||||
if (this.duration == 0) {
|
||||
this.startTime += delay;
|
||||
} else {
|
||||
this.forwardTime(this.currentTime + delay);
|
||||
}
|
||||
}
|
||||
|
||||
fork(element: any, currentTime?: number): TimelineBuilder {
|
||||
this.applyStylesToKeyframe();
|
||||
return new TimelineBuilder(
|
||||
element, currentTime || this.currentTime, this._elementTimelineStylesLookup);
|
||||
}
|
||||
|
||||
private _loadKeyframe() {
|
||||
if (this._currentKeyframe) {
|
||||
this._previousKeyframe = this._currentKeyframe;
|
||||
}
|
||||
this._currentKeyframe = this._keyframes.get(this.duration) !;
|
||||
if (!this._currentKeyframe) {
|
||||
this._currentKeyframe = Object.create(this._backFill, {});
|
||||
this._keyframes.set(this.duration, this._currentKeyframe);
|
||||
}
|
||||
}
|
||||
|
||||
forwardFrame() {
|
||||
this.duration += ONE_FRAME_IN_MILLISECONDS;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
forwardTime(time: number) {
|
||||
this.applyStylesToKeyframe();
|
||||
this.duration = time;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
private _updateStyle(prop: string, value: string|number) {
|
||||
this._localTimelineStyles[prop] = value;
|
||||
this._globalTimelineStyles[prop] = value;
|
||||
this._styleSummary[prop] = {time: this.currentTime, value};
|
||||
}
|
||||
|
||||
allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; }
|
||||
|
||||
applyEmptyStep(easing: string|null) {
|
||||
if (easing) {
|
||||
this._previousKeyframe['easing'] = easing;
|
||||
}
|
||||
|
||||
// special case for animate(duration):
|
||||
// all missing styles are filled with a `*` value then
|
||||
// if any destination styles are filled in later on the same
|
||||
// keyframe then they will override the overridden styles
|
||||
// We use `_globalTimelineStyles` here because there may be
|
||||
// styles in previous keyframes that are not present in this timeline
|
||||
Object.keys(this._globalTimelineStyles).forEach(prop => {
|
||||
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
|
||||
this._currentKeyframe[prop] = AUTO_STYLE;
|
||||
});
|
||||
this._currentEmptyStepKeyframe = this._currentKeyframe;
|
||||
}
|
||||
|
||||
setStyles(
|
||||
input: (ɵStyleData|string)[], easing: string|null, errors: any[],
|
||||
options?: AnimationOptions) {
|
||||
if (easing) {
|
||||
this._previousKeyframe['easing'] = easing;
|
||||
}
|
||||
|
||||
const params = (options && options.params) || {};
|
||||
const styles = flattenStyles(input, this._globalTimelineStyles);
|
||||
Object.keys(styles).forEach(prop => {
|
||||
const val = interpolateParams(styles[prop], params, errors);
|
||||
this._pendingStyles[prop] = val;
|
||||
if (!this._localTimelineStyles.hasOwnProperty(prop)) {
|
||||
this._backFill[prop] = this._globalTimelineStyles.hasOwnProperty(prop) ?
|
||||
this._globalTimelineStyles[prop] :
|
||||
AUTO_STYLE;
|
||||
}
|
||||
this._updateStyle(prop, val);
|
||||
});
|
||||
}
|
||||
|
||||
applyStylesToKeyframe() {
|
||||
const styles = this._pendingStyles;
|
||||
const props = Object.keys(styles);
|
||||
if (props.length == 0) return;
|
||||
|
||||
this._pendingStyles = {};
|
||||
|
||||
props.forEach(prop => {
|
||||
const val = styles[prop];
|
||||
this._currentKeyframe[prop] = val;
|
||||
});
|
||||
|
||||
Object.keys(this._localTimelineStyles).forEach(prop => {
|
||||
if (!this._currentKeyframe.hasOwnProperty(prop)) {
|
||||
this._currentKeyframe[prop] = this._localTimelineStyles[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
snapshotCurrentStyles() {
|
||||
Object.keys(this._localTimelineStyles).forEach(prop => {
|
||||
const val = this._localTimelineStyles[prop];
|
||||
this._pendingStyles[prop] = val;
|
||||
this._updateStyle(prop, val);
|
||||
});
|
||||
}
|
||||
|
||||
getFinalKeyframe() { return this._keyframes.get(this.duration); }
|
||||
|
||||
get properties() {
|
||||
const properties: string[] = [];
|
||||
for (let prop in this._currentKeyframe) {
|
||||
properties.push(prop);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
mergeTimelineCollectedStyles(timeline: TimelineBuilder) {
|
||||
Object.keys(timeline._styleSummary).forEach(prop => {
|
||||
const details0 = this._styleSummary[prop];
|
||||
const details1 = timeline._styleSummary[prop];
|
||||
if (!details0 || details1.time > details0.time) {
|
||||
this._updateStyle(prop, details1.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildKeyframes(): AnimationTimelineInstruction {
|
||||
this.applyStylesToKeyframe();
|
||||
const preStyleProps = new Set<string>();
|
||||
const postStyleProps = new Set<string>();
|
||||
const isEmpty = this._keyframes.size === 1 && this.duration === 0;
|
||||
|
||||
let finalKeyframes: ɵStyleData[] = [];
|
||||
this._keyframes.forEach((keyframe, time) => {
|
||||
const finalKeyframe = copyStyles(keyframe, true);
|
||||
Object.keys(finalKeyframe).forEach(prop => {
|
||||
const value = finalKeyframe[prop];
|
||||
if (value == PRE_STYLE) {
|
||||
preStyleProps.add(prop);
|
||||
} else if (value == AUTO_STYLE) {
|
||||
postStyleProps.add(prop);
|
||||
}
|
||||
});
|
||||
if (!isEmpty) {
|
||||
finalKeyframe['offset'] = time / this.duration;
|
||||
}
|
||||
finalKeyframes.push(finalKeyframe);
|
||||
});
|
||||
|
||||
const preProps: string[] = preStyleProps.size ? iteratorToArray(preStyleProps.values()) : [];
|
||||
const postProps: string[] = postStyleProps.size ? iteratorToArray(postStyleProps.values()) : [];
|
||||
|
||||
// special case for a 0-second animation (which is designed just to place styles onscreen)
|
||||
if (isEmpty) {
|
||||
const kf0 = finalKeyframes[0];
|
||||
const kf1 = copyObj(kf0);
|
||||
kf0['offset'] = 0;
|
||||
kf1['offset'] = 1;
|
||||
finalKeyframes = [kf0, kf1];
|
||||
}
|
||||
|
||||
return createTimelineInstruction(
|
||||
this.element, finalKeyframes, preProps, postProps, this.duration, this.startTime,
|
||||
this.easing, false);
|
||||
}
|
||||
}
|
||||
|
||||
class SubTimelineBuilder extends TimelineBuilder {
|
||||
public timings: AnimateTimings;
|
||||
|
||||
constructor(
|
||||
public element: any, public keyframes: ɵStyleData[], public preStyleProps: string[],
|
||||
public postStyleProps: string[], timings: AnimateTimings,
|
||||
private _stretchStartingKeyframe: boolean = false) {
|
||||
super(element, timings.delay);
|
||||
this.timings = {duration: timings.duration, delay: timings.delay, easing: timings.easing};
|
||||
}
|
||||
|
||||
containsAnimation(): boolean { return this.keyframes.length > 1; }
|
||||
|
||||
buildKeyframes(): AnimationTimelineInstruction {
|
||||
let keyframes = this.keyframes;
|
||||
let {delay, duration, easing} = this.timings;
|
||||
if (this._stretchStartingKeyframe && delay) {
|
||||
const newKeyframes: ɵStyleData[] = [];
|
||||
const totalTime = duration + delay;
|
||||
const startingGap = delay / totalTime;
|
||||
|
||||
// the original starting keyframe now starts once the delay is done
|
||||
const newFirstKeyframe = copyStyles(keyframes[0], false);
|
||||
newFirstKeyframe['offset'] = 0;
|
||||
newKeyframes.push(newFirstKeyframe);
|
||||
|
||||
const oldFirstKeyframe = copyStyles(keyframes[0], false);
|
||||
oldFirstKeyframe['offset'] = roundOffset(startingGap);
|
||||
newKeyframes.push(oldFirstKeyframe);
|
||||
|
||||
/*
|
||||
When the keyframe is stretched then it means that the delay before the animation
|
||||
starts is gone. Instead the first keyframe is placed at the start of the animation
|
||||
and it is then copied to where it starts when the original delay is over. This basically
|
||||
means nothing animates during that delay, but the styles are still renderered. For this
|
||||
to work the original offset values that exist in the original keyframes must be "warped"
|
||||
so that they can take the new keyframe + delay into account.
|
||||
|
||||
delay=1000, duration=1000, keyframes = 0 .5 1
|
||||
|
||||
turns into
|
||||
|
||||
delay=0, duration=2000, keyframes = 0 .33 .66 1
|
||||
*/
|
||||
|
||||
// offsets between 1 ... n -1 are all warped by the keyframe stretch
|
||||
const limit = keyframes.length - 1;
|
||||
for (let i = 1; i <= limit; i++) {
|
||||
let kf = copyStyles(keyframes[i], false);
|
||||
const oldOffset = kf['offset'] as number;
|
||||
const timeAtKeyframe = delay + oldOffset * duration;
|
||||
kf['offset'] = roundOffset(timeAtKeyframe / totalTime);
|
||||
newKeyframes.push(kf);
|
||||
}
|
||||
|
||||
// the new starting keyframe should be added at the start
|
||||
duration = totalTime;
|
||||
delay = 0;
|
||||
easing = '';
|
||||
|
||||
keyframes = newKeyframes;
|
||||
}
|
||||
|
||||
return createTimelineInstruction(
|
||||
this.element, keyframes, this.preStyleProps, this.postStyleProps, duration, delay, easing,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeQuery(
|
||||
rootElement: any, selector: string, originalSelector: string, limit: number,
|
||||
includeSelf: boolean, optional: boolean, errors: any[]): any[] {
|
||||
const multi = limit != 1;
|
||||
let results: any[] = [];
|
||||
if (includeSelf) {
|
||||
results.push(rootElement);
|
||||
}
|
||||
if (selector.length > 0) { // if :self is only used then the selector is empty
|
||||
if (multi) {
|
||||
results.push(...rootElement.querySelectorAll(selector));
|
||||
if (limit > 1) {
|
||||
results = results.slice(0, limit);
|
||||
}
|
||||
} else {
|
||||
const elm = rootElement.querySelector(selector);
|
||||
if (elm) {
|
||||
results.push(elm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!optional && results.length == 0) {
|
||||
errors.push(
|
||||
`\`query("${originalSelector}")\` returned zero elements. (Use \`query("${originalSelector}", { optional: true })\` if you wish to allow this.)`);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function roundOffset(offset: number, decimalPoints = 3): number {
|
||||
const mult = Math.pow(10, decimalPoints - 1);
|
||||
return Math.round(offset * mult) / mult;
|
||||
}
|
||||
|
||||
function flattenStyles(input: (ɵStyleData | string)[], allStyles: ɵStyleData) {
|
||||
const styles: ɵStyleData = {};
|
||||
let allProperties: string[];
|
||||
input.forEach(token => {
|
||||
if (token === '*') {
|
||||
allProperties = allProperties || Object.keys(allStyles);
|
||||
allProperties.forEach(prop => { styles[prop] = AUTO_STYLE; });
|
||||
} else {
|
||||
copyStyles(token as ɵStyleData, false, styles);
|
||||
}
|
||||
});
|
||||
return styles;
|
||||
}
|
@ -9,21 +9,30 @@ import {ɵStyleData} from '@angular/animations';
|
||||
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
|
||||
|
||||
export interface AnimationTimelineInstruction extends AnimationEngineInstruction {
|
||||
element: any;
|
||||
keyframes: ɵStyleData[];
|
||||
preStyleProps: string[];
|
||||
postStyleProps: string[];
|
||||
duration: number;
|
||||
delay: number;
|
||||
totalTime: number;
|
||||
easing: string|null|undefined;
|
||||
easing: string|null;
|
||||
stretchStartingKeyframe?: boolean;
|
||||
subTimeline: boolean;
|
||||
}
|
||||
|
||||
export function createTimelineInstruction(
|
||||
keyframes: ɵStyleData[], duration: number, delay: number,
|
||||
easing: string | null | undefined): AnimationTimelineInstruction {
|
||||
element: any, keyframes: ɵStyleData[], preStyleProps: string[], postStyleProps: string[],
|
||||
duration: number, delay: number, easing: string | null = null,
|
||||
subTimeline: boolean = false): AnimationTimelineInstruction {
|
||||
return {
|
||||
type: AnimationTransitionInstructionType.TimelineAnimation,
|
||||
element,
|
||||
keyframes,
|
||||
preStyleProps,
|
||||
postStyleProps,
|
||||
duration,
|
||||
delay,
|
||||
totalTime: duration + delay, easing
|
||||
totalTime: duration + delay, easing, subTimeline
|
||||
};
|
||||
}
|
||||
|
@ -1,470 +0,0 @@
|
||||
/**
|
||||
* @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, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence, style, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles, normalizeStyles, parseTimeExpression} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
|
||||
* animation DSL code.
|
||||
*
|
||||
* The code below will be converted from:
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ opacity: 0 }),
|
||||
* animate(1000, style({ opacity: 0 }))
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* To:
|
||||
* ```
|
||||
* keyframes = [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }]
|
||||
* duration = 1000
|
||||
* delay = 0
|
||||
* easing = ''
|
||||
* ```
|
||||
*
|
||||
* For this operation to cover the combination of animation verbs (style, animate, group, etc...) a
|
||||
* combination of prototypical inheritance, AST traversal and merge-sort-like algorithms are used.
|
||||
*
|
||||
* [AST Traversal]
|
||||
* Each of the animation verbs, when executed, will return an string-map object representing what
|
||||
* type of action it is (style, animate, group, etc...) and the data associated with it. This means
|
||||
* that when functional composition mix of these functions is evaluated (like in the example above)
|
||||
* then it will end up producing a tree of objects representing the animation itself.
|
||||
*
|
||||
* When this animation object tree is processed by the visitor code below it will visit each of the
|
||||
* verb statements within the visitor. And during each visit it will build the context of the
|
||||
* animation keyframes by interacting with the `TimelineBuilder`.
|
||||
*
|
||||
* [TimelineBuilder]
|
||||
* This class is responsible for tracking the styles and building a series of keyframe objects for a
|
||||
* timeline between a start and end time. The builder starts off with an initial timeline and each
|
||||
* time the AST comes across a `group()`, `keyframes()` or a combination of the two within a
|
||||
* `sequence()` then it will generate a sub timeline for each step as well as a new one after
|
||||
* they are complete.
|
||||
*
|
||||
* As the AST is traversed, the timing state on each of the timelines will be incremented. If a sub
|
||||
* timeline was created (based on one of the cases above) then the parent timeline will attempt to
|
||||
* merge the styles used within the sub timelines into itself (only with group() this will happen).
|
||||
* This happens with a merge operation (much like how the merge works in mergesort) and it will only
|
||||
* copy the most recently used styles from the sub timelines into the parent timeline. This ensures
|
||||
* that if the styles are used later on in another phase of the animation then they will be the most
|
||||
* up-to-date values.
|
||||
*
|
||||
* [How Missing Styles Are Updated]
|
||||
* Each timeline has a `backFill` property which is responsible for filling in new styles into
|
||||
* already processed keyframes if a new style shows up later within the animation sequence.
|
||||
*
|
||||
* ```
|
||||
* sequence([
|
||||
* style({ width: 0 }),
|
||||
* animate(1000, style({ width: 100 })),
|
||||
* animate(1000, style({ width: 200 })),
|
||||
* animate(1000, style({ width: 300 }))
|
||||
* animate(1000, style({ width: 400, height: 400 })) // notice how `height` doesn't exist anywhere
|
||||
* else
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* What is happening here is that the `height` value is added later in the sequence, but is missing
|
||||
* from all previous animation steps. Therefore when a keyframe is created it would also be missing
|
||||
* from all previous keyframes up until where it is first used. For the timeline keyframe generation
|
||||
* to properly fill in the style it will place the previous value (the value from the parent
|
||||
* timeline) or a default value of `*` into the backFill object. Given that each of the keyframe
|
||||
* styles are objects that prototypically inherited from the backFill object, this means that if a
|
||||
* value is added into the backFill then it will automatically propagate any missing values to all
|
||||
* keyframes. Therefore the missing `height` value will be properly filled into the already
|
||||
* processed keyframes.
|
||||
*
|
||||
* When a sub-timeline is created it will have its own backFill property. This is done so that
|
||||
* styles present within the sub-timeline do not accidentally seep into the previous/future timeline
|
||||
* keyframes
|
||||
*
|
||||
* (For prototypically inherited contents to be detected a `for(i in obj)` loop must be used.)
|
||||
*
|
||||
* [Validation]
|
||||
* The code in this file is not responsible for validation. That functionality happens with within
|
||||
* the `AnimationValidatorVisitor` code.
|
||||
*/
|
||||
export function buildAnimationKeyframes(
|
||||
ast: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData = {},
|
||||
finalStyles: ɵStyleData = {}): AnimationTimelineInstruction[] {
|
||||
const normalizedAst =
|
||||
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
|
||||
return new AnimationTimelineVisitor().buildKeyframes(normalizedAst, startingStyles, finalStyles);
|
||||
}
|
||||
|
||||
export declare type StyleAtTime = {
|
||||
time: number; value: string | number;
|
||||
};
|
||||
|
||||
export class AnimationTimelineContext {
|
||||
currentTimeline: TimelineBuilder;
|
||||
currentAnimateTimings: AnimateTimings|null;
|
||||
previousNode: AnimationMetadata = <AnimationMetadata>{};
|
||||
subContextCount = 0;
|
||||
|
||||
constructor(
|
||||
public errors: any[], public timelines: TimelineBuilder[],
|
||||
initialTimeline?: TimelineBuilder) {
|
||||
this.currentTimeline = initialTimeline || new TimelineBuilder(0);
|
||||
timelines.push(this.currentTimeline);
|
||||
}
|
||||
|
||||
createSubContext(): AnimationTimelineContext {
|
||||
const context =
|
||||
new AnimationTimelineContext(this.errors, this.timelines, this.currentTimeline.fork());
|
||||
context.previousNode = this.previousNode;
|
||||
context.currentAnimateTimings = this.currentAnimateTimings;
|
||||
this.subContextCount++;
|
||||
return context;
|
||||
}
|
||||
|
||||
transformIntoNewTimeline(newTime = 0) {
|
||||
this.currentTimeline = this.currentTimeline.fork(newTime);
|
||||
this.timelines.push(this.currentTimeline);
|
||||
return this.currentTimeline;
|
||||
}
|
||||
|
||||
incrementTime(time: number) {
|
||||
this.currentTimeline.forwardTime(this.currentTimeline.duration + time);
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationTimelineVisitor implements AnimationDslVisitor {
|
||||
buildKeyframes(ast: AnimationMetadata, startingStyles: ɵStyleData, finalStyles: ɵStyleData):
|
||||
AnimationTimelineInstruction[] {
|
||||
const context = new AnimationTimelineContext([], []);
|
||||
context.currentTimeline.setStyles(startingStyles);
|
||||
|
||||
visitAnimationNode(this, ast, context);
|
||||
|
||||
// this checks to see if an actual animation happened
|
||||
const timelines = context.timelines.filter(timeline => timeline.hasStyling());
|
||||
if (timelines.length && Object.keys(finalStyles).length) {
|
||||
const tl = timelines[timelines.length - 1];
|
||||
if (!tl.allowOnlyTimelineStyles()) {
|
||||
tl.setStyles(finalStyles);
|
||||
}
|
||||
}
|
||||
|
||||
return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) :
|
||||
[createTimelineInstruction([], 0, 0, '')];
|
||||
}
|
||||
|
||||
visitState(ast: AnimationStateMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: AnimationTimelineContext) {
|
||||
const subContextCount = context.subContextCount;
|
||||
if (context.previousNode.type == AnimationMetadataType.Style) {
|
||||
context.currentTimeline.forwardFrame();
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
ast.steps.forEach(s => visitAnimationNode(this, s, context));
|
||||
|
||||
// this means that some animation function within the sequence
|
||||
// ended up creating a sub timeline (which means the current
|
||||
// timeline cannot overlap with the contents of the sequence)
|
||||
if (context.subContextCount > subContextCount) {
|
||||
context.transformIntoNewTimeline();
|
||||
}
|
||||
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitGroup(ast: AnimationGroupMetadata, context: AnimationTimelineContext) {
|
||||
const innerTimelines: TimelineBuilder[] = [];
|
||||
let furthestTime = context.currentTimeline.currentTime;
|
||||
ast.steps.forEach(s => {
|
||||
const innerContext = context.createSubContext();
|
||||
visitAnimationNode(this, s, innerContext);
|
||||
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
|
||||
innerTimelines.push(innerContext.currentTimeline);
|
||||
});
|
||||
|
||||
// this operation is run after the AST loop because otherwise
|
||||
// if the parent timeline's collected styles were updated then
|
||||
// it would pass in invalid data into the new-to-be forked items
|
||||
innerTimelines.forEach(
|
||||
timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline));
|
||||
context.transformIntoNewTimeline(furthestTime);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationTimelineContext) {
|
||||
const timings = ast.timings.hasOwnProperty('duration') ?
|
||||
<AnimateTimings>ast.timings :
|
||||
parseTimeExpression(<string|number>ast.timings, context.errors);
|
||||
context.currentAnimateTimings = timings;
|
||||
|
||||
if (timings.delay) {
|
||||
context.incrementTime(timings.delay);
|
||||
context.currentTimeline.snapshotCurrentStyles();
|
||||
}
|
||||
|
||||
const astType = ast.styles ? ast.styles.type : -1;
|
||||
if (astType == AnimationMetadataType.KeyframeSequence) {
|
||||
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
|
||||
} else {
|
||||
let styleAst = ast.styles as AnimationStyleMetadata;
|
||||
if (!styleAst) {
|
||||
const newStyleData: {[prop: string]: string | number} = {};
|
||||
if (timings.easing) {
|
||||
newStyleData['easing'] = timings.easing;
|
||||
}
|
||||
styleAst = style(newStyleData);
|
||||
(styleAst as any)['treatAsEmptyStep'] = true;
|
||||
}
|
||||
context.incrementTime(timings.duration);
|
||||
if (styleAst) {
|
||||
this.visitStyle(styleAst, context);
|
||||
}
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
visitStyle(ast: AnimationStyleMetadata, context: AnimationTimelineContext) {
|
||||
// this is a special case when a style() call is issued directly after
|
||||
// a call to animate(). If the clock is not forwarded by one frame then
|
||||
// the style() calls will be merged into the previous animate() call
|
||||
// which is incorrect.
|
||||
if (!context.currentAnimateTimings &&
|
||||
context.previousNode.type == AnimationMetadataType.Animate) {
|
||||
context.currentTimeline.forwardFrame();
|
||||
}
|
||||
|
||||
const normalizedStyles = normalizeStyles(ast.styles);
|
||||
const easing = context.currentAnimateTimings && context.currentAnimateTimings.easing;
|
||||
this._applyStyles(
|
||||
normalizedStyles, easing, (ast as any)['treatAsEmptyStep'] ? true : false, context);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
|
||||
private _applyStyles(
|
||||
styles: ɵStyleData, easing: string|null, treatAsEmptyStep: boolean,
|
||||
context: AnimationTimelineContext) {
|
||||
if (styles.hasOwnProperty('easing')) {
|
||||
easing = easing || styles['easing'] as string;
|
||||
delete styles['easing'];
|
||||
}
|
||||
context.currentTimeline.setStyles(styles, easing, treatAsEmptyStep);
|
||||
}
|
||||
|
||||
visitKeyframeSequence(
|
||||
ast: AnimationKeyframesSequenceMetadata, context: AnimationTimelineContext) {
|
||||
const MAX_KEYFRAME_OFFSET = 1;
|
||||
const limit = ast.steps.length - 1;
|
||||
const firstKeyframe = ast.steps[0];
|
||||
|
||||
let offsetGap = 0;
|
||||
const containsOffsets = getOffset(firstKeyframe) != null;
|
||||
if (!containsOffsets) {
|
||||
offsetGap = MAX_KEYFRAME_OFFSET / limit;
|
||||
}
|
||||
|
||||
const startTime = context.currentTimeline.duration;
|
||||
const duration = context.currentAnimateTimings !.duration;
|
||||
const innerContext = context.createSubContext();
|
||||
const innerTimeline = innerContext.currentTimeline;
|
||||
innerTimeline.easing = context.currentAnimateTimings !.easing;
|
||||
|
||||
ast.steps.forEach((step: AnimationStyleMetadata, i: number) => {
|
||||
const normalizedStyles = normalizeStyles(step.styles);
|
||||
const offset = containsOffsets ?
|
||||
(step.offset != null ? step.offset : parseFloat(normalizedStyles['offset'] as string)) :
|
||||
(i == limit ? MAX_KEYFRAME_OFFSET : i * offsetGap);
|
||||
innerTimeline.forwardTime(offset * duration);
|
||||
this._applyStyles(normalizedStyles, null, false, innerContext);
|
||||
});
|
||||
|
||||
// this will ensure that the parent timeline gets all the styles from
|
||||
// the child even if the new timeline below is not used
|
||||
context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline);
|
||||
|
||||
// we do this because the window between this timeline and the sub timeline
|
||||
// should ensure that the styles within are exactly the same as they were before
|
||||
context.transformIntoNewTimeline(startTime + duration);
|
||||
context.previousNode = ast;
|
||||
}
|
||||
}
|
||||
|
||||
export class TimelineBuilder {
|
||||
public duration: number = 0;
|
||||
public easing: string|null = '';
|
||||
private _previousKeyframe: ɵStyleData = {};
|
||||
private _currentKeyframe: ɵStyleData;
|
||||
private _keyframes = new Map<number, ɵStyleData>();
|
||||
private _styleSummary: {[prop: string]: StyleAtTime} = {};
|
||||
private _localTimelineStyles: ɵStyleData;
|
||||
private _backFill: ɵStyleData = {};
|
||||
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
|
||||
private _globalTimelineStyles: ɵStyleData;
|
||||
|
||||
constructor(public startTime: number, globalTimelineStyles?: ɵStyleData) {
|
||||
this._localTimelineStyles = Object.create(this._backFill, {});
|
||||
this._globalTimelineStyles =
|
||||
globalTimelineStyles ? globalTimelineStyles : this._localTimelineStyles;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
hasStyling(): boolean { return this._keyframes.size > 1; }
|
||||
|
||||
get currentTime() { return this.startTime + this.duration; }
|
||||
|
||||
fork(currentTime = 0): TimelineBuilder {
|
||||
return new TimelineBuilder(currentTime || this.currentTime, this._globalTimelineStyles);
|
||||
}
|
||||
|
||||
private _loadKeyframe() {
|
||||
if (this._currentKeyframe) {
|
||||
this._previousKeyframe = this._currentKeyframe;
|
||||
}
|
||||
this._currentKeyframe = this._keyframes.get(this.duration) !;
|
||||
if (!this._currentKeyframe) {
|
||||
this._currentKeyframe = Object.create(this._backFill, {});
|
||||
this._keyframes.set(this.duration, this._currentKeyframe);
|
||||
}
|
||||
}
|
||||
|
||||
forwardFrame() {
|
||||
this.duration++;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
forwardTime(time: number) {
|
||||
this.duration = time;
|
||||
this._loadKeyframe();
|
||||
}
|
||||
|
||||
private _updateStyle(prop: string, value: string|number) {
|
||||
this._localTimelineStyles[prop] = value;
|
||||
this._globalTimelineStyles ![prop] = value;
|
||||
this._styleSummary[prop] = {time: this.currentTime, value};
|
||||
}
|
||||
|
||||
allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; }
|
||||
|
||||
setStyles(styles: ɵStyleData, easing: string|null = null, treatAsEmptyStep: boolean = false) {
|
||||
if (easing) {
|
||||
this._previousKeyframe !['easing'] = easing;
|
||||
}
|
||||
|
||||
if (treatAsEmptyStep) {
|
||||
// special case for animate(duration):
|
||||
// all missing styles are filled with a `*` value then
|
||||
// if any destination styles are filled in later on the same
|
||||
// keyframe then they will override the overridden styles
|
||||
// We use `_globalTimelineStyles` here because there may be
|
||||
// styles in previous keyframes that are not present in this timeline
|
||||
Object.keys(this._globalTimelineStyles).forEach(prop => {
|
||||
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
|
||||
this._currentKeyframe[prop] = AUTO_STYLE;
|
||||
});
|
||||
this._currentEmptyStepKeyframe = this._currentKeyframe;
|
||||
} else {
|
||||
Object.keys(styles).forEach(prop => {
|
||||
if (prop !== 'offset') {
|
||||
const val = styles[prop];
|
||||
this._currentKeyframe[prop] = val;
|
||||
if (!this._localTimelineStyles[prop]) {
|
||||
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
|
||||
}
|
||||
this._updateStyle(prop, val);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(this._localTimelineStyles).forEach(prop => {
|
||||
if (!this._currentKeyframe.hasOwnProperty(prop)) {
|
||||
this._currentKeyframe[prop] = this._localTimelineStyles[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
snapshotCurrentStyles() { copyStyles(this._localTimelineStyles, false, this._currentKeyframe); }
|
||||
|
||||
getFinalKeyframe(): ɵStyleData { return this._keyframes.get(this.duration) !; }
|
||||
|
||||
get properties() {
|
||||
const properties: string[] = [];
|
||||
for (let prop in this._currentKeyframe) {
|
||||
properties.push(prop);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
mergeTimelineCollectedStyles(timeline: TimelineBuilder) {
|
||||
Object.keys(timeline._styleSummary).forEach(prop => {
|
||||
const details0 = this._styleSummary[prop];
|
||||
const details1 = timeline._styleSummary[prop];
|
||||
if (!details0 || details1.time > details0.time) {
|
||||
this._updateStyle(prop, details1.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildKeyframes(): AnimationTimelineInstruction {
|
||||
const finalKeyframes: ɵStyleData[] = [];
|
||||
// special case for when there are only start/destination
|
||||
// styles but no actual animation animate steps...
|
||||
if (this.duration == 0) {
|
||||
const targetKeyframe = this.getFinalKeyframe();
|
||||
|
||||
const firstKeyframe = copyStyles(targetKeyframe, true);
|
||||
firstKeyframe['offset'] = 0;
|
||||
finalKeyframes.push(firstKeyframe);
|
||||
|
||||
const lastKeyframe = copyStyles(targetKeyframe, true);
|
||||
lastKeyframe['offset'] = 1;
|
||||
finalKeyframes.push(lastKeyframe);
|
||||
} else {
|
||||
this._keyframes.forEach((keyframe, time) => {
|
||||
const finalKeyframe = copyStyles(keyframe, true);
|
||||
finalKeyframe['offset'] = time / this.duration;
|
||||
finalKeyframes.push(finalKeyframe);
|
||||
});
|
||||
}
|
||||
|
||||
return createTimelineInstruction(finalKeyframes, this.duration, this.startTime, this.easing);
|
||||
}
|
||||
}
|
||||
|
||||
function getOffset(ast: AnimationStyleMetadata): number {
|
||||
let offset = ast.offset;
|
||||
if (offset == null) {
|
||||
const styles = ast.styles;
|
||||
if (Array.isArray(styles)) {
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
const o = styles[i]['offset'] as number;
|
||||
if (o != null) {
|
||||
offset = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
offset = styles['offset'] as number;
|
||||
}
|
||||
}
|
||||
return offset !;
|
||||
}
|
@ -57,8 +57,16 @@ function parseAnimationAlias(alias: string, errors: string[]): string {
|
||||
|
||||
function makeLambdaFromStates(lhs: string, rhs: string): TransitionMatcherFn {
|
||||
return (fromState: any, toState: any): boolean => {
|
||||
const lhsMatch = lhs == ANY_STATE || lhs == fromState;
|
||||
const rhsMatch = rhs == ANY_STATE || rhs == toState;
|
||||
let lhsMatch = lhs == ANY_STATE || lhs == fromState;
|
||||
let rhsMatch = rhs == ANY_STATE || rhs == toState;
|
||||
|
||||
if (!lhsMatch && typeof fromState === 'boolean') {
|
||||
lhsMatch = fromState ? lhs === 'true' : lhs === 'false';
|
||||
}
|
||||
if (!rhsMatch && typeof toState === 'boolean') {
|
||||
rhsMatch = toState ? rhs === 'true' : rhs === 'false';
|
||||
}
|
||||
|
||||
return lhsMatch && rhsMatch;
|
||||
};
|
||||
}
|
||||
|
@ -5,38 +5,66 @@
|
||||
* 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 {AnimationMetadata, AnimationTransitionMetadata, sequence, ɵStyleData} from '@angular/animations';
|
||||
import {AnimationOptions, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {buildAnimationKeyframes} from './animation_timeline_visitor';
|
||||
import {getOrSetAsInMap} from '../render/shared';
|
||||
import {iteratorToArray, mergeAnimationOptions} from '../util';
|
||||
|
||||
import {TransitionAst} from './animation_ast';
|
||||
import {buildAnimationTimelines} from './animation_timeline_builder';
|
||||
import {TransitionMatcherFn} from './animation_transition_expr';
|
||||
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
|
||||
import {ElementInstructionMap} from './element_instruction_map';
|
||||
|
||||
export class AnimationTransitionFactory {
|
||||
private _animationAst: AnimationMetadata;
|
||||
|
||||
constructor(
|
||||
private _triggerName: string, ast: AnimationTransitionMetadata,
|
||||
private matchFns: TransitionMatcherFn[],
|
||||
private _stateStyles: {[stateName: string]: ɵStyleData}) {
|
||||
const normalizedAst = Array.isArray(ast.animation) ?
|
||||
sequence(<AnimationMetadata[]>ast.animation) :
|
||||
<AnimationMetadata>ast.animation;
|
||||
this._animationAst = normalizedAst;
|
||||
private _triggerName: string, public ast: TransitionAst,
|
||||
private _stateStyles: {[stateName: string]: ɵStyleData}) {}
|
||||
|
||||
match(currentState: any, nextState: any): boolean {
|
||||
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState);
|
||||
}
|
||||
|
||||
match(currentState: any, nextState: any): AnimationTransitionInstruction|undefined {
|
||||
if (!oneOrMoreTransitionsMatch(this.matchFns, currentState, nextState)) return;
|
||||
build(
|
||||
element: any, currentState: any, nextState: any, options?: AnimationOptions,
|
||||
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction|undefined {
|
||||
const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {});
|
||||
|
||||
const backupStateStyles = this._stateStyles['*'] || {};
|
||||
const currentStateStyles = this._stateStyles[currentState] || backupStateStyles;
|
||||
const nextStateStyles = this._stateStyles[nextState] || backupStateStyles;
|
||||
|
||||
const timelines =
|
||||
buildAnimationKeyframes(this._animationAst, currentStateStyles, nextStateStyles);
|
||||
const errors: any[] = [];
|
||||
const timelines = buildAnimationTimelines(
|
||||
element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
|
||||
subInstructions, errors);
|
||||
|
||||
if (errors.length) {
|
||||
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
|
||||
const queriedElements = new Set<any>();
|
||||
timelines.forEach(tl => {
|
||||
const elm = tl.element;
|
||||
const preProps = getOrSetAsInMap(preStyleMap, elm, {});
|
||||
tl.preStyleProps.forEach(prop => preProps[prop] = true);
|
||||
|
||||
const postProps = getOrSetAsInMap(postStyleMap, elm, {});
|
||||
tl.postStyleProps.forEach(prop => postProps[prop] = true);
|
||||
|
||||
if (elm !== element) {
|
||||
queriedElements.add(elm);
|
||||
}
|
||||
});
|
||||
|
||||
const queriedElementsList = iteratorToArray(queriedElements.values());
|
||||
return createTransitionInstruction(
|
||||
this._triggerName, currentState, nextState, nextState === 'void', currentStateStyles,
|
||||
nextStateStyles, timelines);
|
||||
element, this._triggerName, currentState, nextState, nextState === 'void',
|
||||
currentStateStyles, nextStateStyles, timelines, queriedElementsList, preStyleMap,
|
||||
postStyleMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '..
|
||||
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
export interface AnimationTransitionInstruction extends AnimationEngineInstruction {
|
||||
element: any;
|
||||
triggerName: string;
|
||||
isRemovalTransition: boolean;
|
||||
fromState: string;
|
||||
@ -17,20 +18,29 @@ export interface AnimationTransitionInstruction extends AnimationEngineInstructi
|
||||
toState: string;
|
||||
toStyles: ɵStyleData;
|
||||
timelines: AnimationTimelineInstruction[];
|
||||
queriedElements: any[];
|
||||
preStyleProps: Map<any, {[prop: string]: boolean}>;
|
||||
postStyleProps: Map<any, {[prop: string]: boolean}>;
|
||||
}
|
||||
|
||||
export function createTransitionInstruction(
|
||||
triggerName: string, fromState: string, toState: string, isRemovalTransition: boolean,
|
||||
fromStyles: ɵStyleData, toStyles: ɵStyleData,
|
||||
timelines: AnimationTimelineInstruction[]): AnimationTransitionInstruction {
|
||||
element: any, triggerName: string, fromState: string, toState: string,
|
||||
isRemovalTransition: boolean, fromStyles: ɵStyleData, toStyles: ɵStyleData,
|
||||
timelines: AnimationTimelineInstruction[], queriedElements: any[],
|
||||
preStyleProps: Map<any, {[prop: string]: boolean}>,
|
||||
postStyleProps: Map<any, {[prop: string]: boolean}>): AnimationTransitionInstruction {
|
||||
return {
|
||||
type: AnimationTransitionInstructionType.TransitionAnimation,
|
||||
element,
|
||||
triggerName,
|
||||
isRemovalTransition,
|
||||
fromState,
|
||||
fromStyles,
|
||||
toState,
|
||||
toStyles,
|
||||
timelines
|
||||
timelines,
|
||||
queriedElements,
|
||||
preStyleProps,
|
||||
postStyleProps
|
||||
};
|
||||
}
|
||||
|
@ -5,22 +5,18 @@
|
||||
* 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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations';
|
||||
import {ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {copyStyles, normalizeStyles} from '../util';
|
||||
import {copyStyles} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
import {parseTransitionExpr} from './animation_transition_expr';
|
||||
import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast';
|
||||
import {AnimationTransitionFactory} from './animation_transition_factory';
|
||||
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
|
||||
import {validateAnimationSequence} from './animation_validator_visitor';
|
||||
|
||||
|
||||
/**
|
||||
* @experimental Animation support is experimental.
|
||||
*/
|
||||
export function buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
|
||||
return new AnimationTriggerVisitor().buildTrigger(name, definitions);
|
||||
export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger {
|
||||
return new AnimationTrigger(name, ast);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,90 +24,51 @@ export function buildTrigger(name: string, definitions: AnimationMetadata[]): An
|
||||
*/
|
||||
export class AnimationTrigger {
|
||||
public transitionFactories: AnimationTransitionFactory[] = [];
|
||||
public fallbackTransition: AnimationTransitionFactory;
|
||||
public states: {[stateName: string]: ɵStyleData} = {};
|
||||
|
||||
constructor(
|
||||
public name: string, states: {[stateName: string]: ɵStyleData},
|
||||
private _transitionAsts: AnimationTransitionMetadata[]) {
|
||||
Object.keys(states).forEach(
|
||||
stateName => { this.states[stateName] = copyStyles(states[stateName], false); });
|
||||
|
||||
const errors: string[] = [];
|
||||
_transitionAsts.forEach(ast => {
|
||||
const exprs = parseTransitionExpr(ast.expr, errors);
|
||||
const sequenceErrors = validateAnimationSequence(ast);
|
||||
if (sequenceErrors.length) {
|
||||
errors.push(...sequenceErrors);
|
||||
} else {
|
||||
this.transitionFactories.push(
|
||||
new AnimationTransitionFactory(this.name, ast, exprs, states));
|
||||
}
|
||||
constructor(public name: string, public ast: TriggerAst) {
|
||||
ast.states.forEach(ast => {
|
||||
const obj = this.states[ast.name] = {};
|
||||
ast.style.styles.forEach(styleTuple => {
|
||||
if (typeof styleTuple == 'object') {
|
||||
copyStyles(styleTuple as ɵStyleData, false, obj);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Animation parsing for the ${name} trigger have failed:${LINE_START}${errors.join(LINE_START)}`);
|
||||
balanceProperties(this.states, 'true', '1');
|
||||
balanceProperties(this.states, 'false', '0');
|
||||
|
||||
ast.transitions.forEach(ast => {
|
||||
this.transitionFactories.push(new AnimationTransitionFactory(name, ast, this.states));
|
||||
});
|
||||
|
||||
this.fallbackTransition = createFallbackTransition(name, this.states);
|
||||
}
|
||||
|
||||
get containsQueries() { return this.ast.queryCount > 0; }
|
||||
|
||||
matchTransition(currentState: any, nextState: any): AnimationTransitionFactory|null {
|
||||
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
|
||||
return entry || null;
|
||||
}
|
||||
}
|
||||
|
||||
function createFallbackTransition(
|
||||
triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory {
|
||||
const matchers = [(fromState: any, toState: any) => true];
|
||||
const animation = new SequenceAst([]);
|
||||
const transition = new TransitionAst(matchers, animation);
|
||||
return new AnimationTransitionFactory(triggerName, transition, states);
|
||||
}
|
||||
|
||||
function balanceProperties(obj: {[key: string]: any}, key1: string, key2: string) {
|
||||
if (obj.hasOwnProperty(key1)) {
|
||||
if (!obj.hasOwnProperty(key2)) {
|
||||
obj[key2] = obj[key1];
|
||||
}
|
||||
}
|
||||
|
||||
createFallbackInstruction(currentState: any, nextState: any): AnimationTransitionInstruction {
|
||||
const backupStateStyles = this.states['*'] || {};
|
||||
const currentStateStyles = this.states[currentState] || backupStateStyles;
|
||||
const nextStateStyles = this.states[nextState] || backupStateStyles;
|
||||
return createTransitionInstruction(
|
||||
this.name, currentState, nextState, nextState == 'void', currentStateStyles,
|
||||
nextStateStyles, []);
|
||||
}
|
||||
|
||||
matchTransition(currentState: any, nextState: any): AnimationTransitionInstruction|null {
|
||||
for (let i = 0; i < this.transitionFactories.length; i++) {
|
||||
let result = this.transitionFactories[i].match(currentState, nextState);
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationTriggerContext {
|
||||
public errors: string[] = [];
|
||||
public states: {[stateName: string]: ɵStyleData} = {};
|
||||
public transitions: AnimationTransitionMetadata[] = [];
|
||||
}
|
||||
|
||||
class AnimationTriggerVisitor implements AnimationDslVisitor {
|
||||
buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
|
||||
const context = new AnimationTriggerContext();
|
||||
definitions.forEach(def => visitAnimationNode(this, def, context));
|
||||
return new AnimationTrigger(name, context.states, context.transitions);
|
||||
}
|
||||
|
||||
visitState(ast: AnimationStateMetadata, context: any): any {
|
||||
const styles = normalizeStyles(ast.styles.styles);
|
||||
ast.name.split(/\s*,\s*/).forEach(name => { context.states[name] = styles; });
|
||||
}
|
||||
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
|
||||
context.transitions.push(ast);
|
||||
}
|
||||
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitGroup(ast: AnimationGroupMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitStyle(ast: AnimationStyleMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any) {
|
||||
// these values are not visited in this AST
|
||||
} else if (obj.hasOwnProperty(key2)) {
|
||||
obj[key1] = obj[key2];
|
||||
}
|
||||
}
|
||||
|
@ -1,195 +0,0 @@
|
||||
/**
|
||||
* @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 {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence} from '@angular/animations';
|
||||
|
||||
import {normalizeStyles, parseTimeExpression} from '../util';
|
||||
|
||||
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
|
||||
|
||||
export type StyleTimeTuple = {
|
||||
startTime: number; endTime: number;
|
||||
};
|
||||
|
||||
/*
|
||||
* [Validation]
|
||||
* The visitor code below will traverse the animation AST generated by the animation verb functions
|
||||
* (the output is a tree of objects) and attempt to perform a series of validations on the data. The
|
||||
* following corner-cases will be validated:
|
||||
*
|
||||
* 1. Overlap of animations
|
||||
* Given that a CSS property cannot be animated in more than one place at the same time, it's
|
||||
* important that this behaviour is detected and validated. The way in which this occurs is that
|
||||
* each time a style property is examined, a string-map containing the property will be updated with
|
||||
* the start and end times for when the property is used within an animation step.
|
||||
*
|
||||
* If there are two or more parallel animations that are currently running (these are invoked by the
|
||||
* group()) on the same element then the validator will throw an error. Since the start/end timing
|
||||
* values are collected for each property then if the current animation step is animating the same
|
||||
* property and its timing values fall anywhere into the window of time that the property is
|
||||
* currently being animated within then this is what causes an error.
|
||||
*
|
||||
* 2. Timing values
|
||||
* The validator will validate to see if a timing value of `duration delay easing` or
|
||||
* `durationNumber` is valid or not.
|
||||
*
|
||||
* (note that upon validation the code below will replace the timing data with an object containing
|
||||
* {duration,delay,easing}.
|
||||
*
|
||||
* 3. Offset Validation
|
||||
* Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
|
||||
* Offsets within keyframes() are considered valid when:
|
||||
*
|
||||
* - No offsets are used at all
|
||||
* - Each style() entry contains an offset value
|
||||
* - Each offset is between 0 and 1
|
||||
* - Each offset is greater to or equal than the previous one
|
||||
*
|
||||
* Otherwise an error will be thrown.
|
||||
*/
|
||||
export function validateAnimationSequence(ast: AnimationMetadata) {
|
||||
const normalizedAst =
|
||||
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
|
||||
return new AnimationValidatorVisitor().validate(normalizedAst);
|
||||
}
|
||||
|
||||
export class AnimationValidatorVisitor implements AnimationDslVisitor {
|
||||
validate(ast: AnimationMetadata): string[] {
|
||||
const context = new AnimationValidatorContext();
|
||||
visitAnimationNode(this, ast, context);
|
||||
return context.errors;
|
||||
}
|
||||
|
||||
visitState(ast: AnimationStateMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
|
||||
// these values are not visited in this AST
|
||||
}
|
||||
|
||||
visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any {
|
||||
ast.steps.forEach(step => visitAnimationNode(this, step, context));
|
||||
}
|
||||
|
||||
visitGroup(ast: AnimationGroupMetadata, context: AnimationValidatorContext): any {
|
||||
const currentTime = context.currentTime;
|
||||
let furthestTime = 0;
|
||||
ast.steps.forEach(step => {
|
||||
context.currentTime = currentTime;
|
||||
visitAnimationNode(this, step, context);
|
||||
furthestTime = Math.max(furthestTime, context.currentTime);
|
||||
});
|
||||
context.currentTime = furthestTime;
|
||||
}
|
||||
|
||||
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationValidatorContext): any {
|
||||
// we reassign the timings here so that they are not reparsed each
|
||||
// time an animation occurs
|
||||
context.currentAnimateTimings = ast.timings =
|
||||
parseTimeExpression(<string|number>ast.timings, context.errors);
|
||||
|
||||
const astType = ast.styles && ast.styles.type;
|
||||
if (astType == AnimationMetadataType.KeyframeSequence) {
|
||||
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
|
||||
} else {
|
||||
context.currentTime +=
|
||||
context.currentAnimateTimings.duration + context.currentAnimateTimings.delay;
|
||||
if (astType == AnimationMetadataType.Style) {
|
||||
this.visitStyle(<AnimationStyleMetadata>ast.styles, context);
|
||||
}
|
||||
}
|
||||
|
||||
context.currentAnimateTimings = null;
|
||||
}
|
||||
|
||||
visitStyle(ast: AnimationStyleMetadata, context: AnimationValidatorContext): any {
|
||||
const styleData = normalizeStyles(ast.styles);
|
||||
const timings = context.currentAnimateTimings;
|
||||
let endTime = context.currentTime;
|
||||
let startTime = context.currentTime;
|
||||
if (timings && startTime > 0) {
|
||||
startTime -= timings.duration + timings.delay;
|
||||
}
|
||||
Object.keys(styleData).forEach(prop => {
|
||||
const collectedEntry = context.collectedStyles[prop];
|
||||
let updateCollectedStyle = true;
|
||||
if (collectedEntry) {
|
||||
if (startTime != endTime && startTime >= collectedEntry.startTime &&
|
||||
endTime <= collectedEntry.endTime) {
|
||||
context.errors.push(
|
||||
`The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
|
||||
updateCollectedStyle = false;
|
||||
}
|
||||
|
||||
// we always choose the smaller start time value since we
|
||||
// want to have a record of the entire animation window where
|
||||
// the style property is being animated in between
|
||||
startTime = collectedEntry.startTime;
|
||||
}
|
||||
if (updateCollectedStyle) {
|
||||
context.collectedStyles[prop] = {startTime, endTime};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
visitKeyframeSequence(
|
||||
ast: AnimationKeyframesSequenceMetadata, context: AnimationValidatorContext): any {
|
||||
let totalKeyframesWithOffsets = 0;
|
||||
const offsets: number[] = [];
|
||||
let offsetsOutOfOrder = false;
|
||||
let keyframesOutOfRange = false;
|
||||
let previousOffset: number = 0;
|
||||
ast.steps.forEach(step => {
|
||||
const styleData = normalizeStyles(step.styles);
|
||||
let offset = 0;
|
||||
if (styleData.hasOwnProperty('offset')) {
|
||||
totalKeyframesWithOffsets++;
|
||||
offset = <number>styleData['offset'];
|
||||
}
|
||||
keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
|
||||
offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
|
||||
previousOffset = offset;
|
||||
offsets.push(offset);
|
||||
});
|
||||
|
||||
if (keyframesOutOfRange) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
|
||||
}
|
||||
|
||||
if (offsetsOutOfOrder) {
|
||||
context.errors.push(`Please ensure that all keyframe offsets are in order`);
|
||||
}
|
||||
|
||||
const length = ast.steps.length;
|
||||
let generatedOffset = 0;
|
||||
if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
|
||||
context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
|
||||
} else if (totalKeyframesWithOffsets == 0) {
|
||||
generatedOffset = 1 / length;
|
||||
}
|
||||
|
||||
const limit = length - 1;
|
||||
const currentTime = context.currentTime;
|
||||
const animateDuration = context.currentAnimateTimings !.duration;
|
||||
ast.steps.forEach((step, i) => {
|
||||
const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
|
||||
const durationUpToThisFrame = offset * animateDuration;
|
||||
context.currentTime =
|
||||
currentTime + context.currentAnimateTimings !.delay + durationUpToThisFrame;
|
||||
context.currentAnimateTimings !.duration = durationUpToThisFrame;
|
||||
this.visitStyle(step, context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimationValidatorContext {
|
||||
public errors: string[] = [];
|
||||
public currentTime: number = 0;
|
||||
public currentAnimateTimings: AnimateTimings|null;
|
||||
public collectedStyles: {[propName: string]: StyleTimeTuple} = {};
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @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 {AnimationTimelineInstruction} from './animation_timeline_instruction';
|
||||
|
||||
export class ElementInstructionMap {
|
||||
private _map = new Map<any, AnimationTimelineInstruction[]>();
|
||||
|
||||
consume(element: any): AnimationTimelineInstruction[] {
|
||||
let instructions = this._map.get(element);
|
||||
if (instructions) {
|
||||
this._map.delete(element);
|
||||
} else {
|
||||
instructions = [];
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
append(element: any, instructions: AnimationTimelineInstruction[]) {
|
||||
let existingInstructions = this._map.get(element);
|
||||
if (!existingInstructions) {
|
||||
this._map.set(element, existingInstructions = []);
|
||||
}
|
||||
existingInstructions.push(...instructions);
|
||||
}
|
||||
|
||||
has(element: any): boolean { return this._map.has(element); }
|
||||
|
||||
clear() { this._map.clear(); }
|
||||
}
|
@ -10,7 +10,7 @@ export {Animation as ɵAnimation} from './dsl/animation';
|
||||
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationStyleNormalizer as ɵNoopAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
|
||||
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
|
||||
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
|
||||
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine';
|
||||
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine_next';
|
||||
export {NoopAnimationEngine as ɵNoopAnimationEngine} from './render/noop_animation_engine';
|
||||
export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
|
||||
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';
|
||||
|
@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ɵStyleData} from '@angular/animations';
|
||||
import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
|
||||
|
||||
|
||||
@ -14,6 +15,10 @@ import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
|
||||
* @experimental
|
||||
*/
|
||||
export class NoopAnimationDriver implements AnimationDriver {
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string {
|
||||
return defaultValue || '';
|
||||
}
|
||||
|
||||
animate(
|
||||
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
|
||||
easing: string, previousPlayers: any[] = []): AnimationPlayer {
|
||||
@ -26,6 +31,9 @@ export class NoopAnimationDriver implements AnimationDriver {
|
||||
*/
|
||||
export abstract class AnimationDriver {
|
||||
static NOOP: AnimationDriver = new NoopAnimationDriver();
|
||||
|
||||
abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
|
||||
|
||||
abstract animate(
|
||||
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
|
||||
easing?: string|null, previousPlayers?: any[]): any;
|
||||
|
@ -1,527 +0,0 @@
|
||||
/**
|
||||
* @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 {AnimationEvent, AnimationPlayer, AnimationTriggerMetadata, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
|
||||
import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction';
|
||||
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {eraseStyles, setStyles} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
|
||||
export interface QueuedAnimationTransitionTuple {
|
||||
element: any;
|
||||
player: AnimationPlayer;
|
||||
triggerName: string;
|
||||
event: AnimationEvent;
|
||||
}
|
||||
|
||||
export interface TriggerListenerTuple {
|
||||
triggerName: string;
|
||||
phase: string;
|
||||
callback: (event: any) => any;
|
||||
}
|
||||
|
||||
const MARKED_FOR_ANIMATION_CLASSNAME = 'ng-animating';
|
||||
const MARKED_FOR_ANIMATION_SELECTOR = '.ng-animating';
|
||||
const MARKED_FOR_REMOVAL = '$$ngRemove';
|
||||
const VOID_STATE = 'void';
|
||||
|
||||
export class DomAnimationEngine {
|
||||
private _flaggedInserts = new Set<any>();
|
||||
private _queuedRemovals = new Map<any, () => any>();
|
||||
private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = [];
|
||||
private _activeTransitionAnimations = new Map<any, {[triggerName: string]: AnimationPlayer}>();
|
||||
private _activeElementAnimations = new Map<any, AnimationPlayer[]>();
|
||||
|
||||
private _elementTriggerStates = new Map<any, {[triggerName: string]: string}>();
|
||||
|
||||
private _triggers: {[triggerName: string]: AnimationTrigger} = Object.create(null);
|
||||
private _triggerListeners = new Map<any, TriggerListenerTuple[]>();
|
||||
|
||||
private _pendingListenerRemovals = new Map<any, TriggerListenerTuple[]>();
|
||||
|
||||
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
|
||||
|
||||
get queuedPlayers(): AnimationPlayer[] {
|
||||
return this._queuedTransitionAnimations.map(q => q.player);
|
||||
}
|
||||
|
||||
get activePlayers(): AnimationPlayer[] {
|
||||
const players: AnimationPlayer[] = [];
|
||||
this._activeElementAnimations.forEach(activePlayers => players.push(...activePlayers));
|
||||
return players;
|
||||
}
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void {
|
||||
name = name || trigger.name;
|
||||
if (this._triggers[name]) {
|
||||
return;
|
||||
}
|
||||
this._triggers[name] = buildTrigger(name, trigger.definitions);
|
||||
}
|
||||
|
||||
onInsert(element: any, domFn: () => any): void {
|
||||
if (element['nodeType'] == 1) {
|
||||
this._flaggedInserts.add(element);
|
||||
}
|
||||
domFn();
|
||||
}
|
||||
|
||||
onRemove(element: any, domFn: () => any): void {
|
||||
if (element['nodeType'] != 1) {
|
||||
domFn();
|
||||
return;
|
||||
}
|
||||
|
||||
let lookupRef = this._elementTriggerStates.get(element);
|
||||
if (lookupRef) {
|
||||
const possibleTriggers = Object.keys(lookupRef);
|
||||
const hasRemoval = possibleTriggers.some(triggerName => {
|
||||
const oldValue = lookupRef ![triggerName];
|
||||
const instruction = this._triggers[triggerName].matchTransition(oldValue, VOID_STATE);
|
||||
return !!instruction;
|
||||
});
|
||||
if (hasRemoval) {
|
||||
element[MARKED_FOR_REMOVAL] = true;
|
||||
this._queuedRemovals.set(element, domFn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// this means that there are no animations to take on this
|
||||
// leave operation therefore we should fire the done|start callbacks
|
||||
if (this._triggerListeners.has(element)) {
|
||||
element[MARKED_FOR_REMOVAL] = true;
|
||||
this._queuedRemovals.set(element, () => {});
|
||||
}
|
||||
this._onRemovalTransition(element).forEach(player => player.destroy());
|
||||
domFn();
|
||||
}
|
||||
|
||||
setProperty(element: any, property: string, value: any): void {
|
||||
const trigger = this._triggers[property];
|
||||
if (!trigger) {
|
||||
throw new Error(`The provided animation trigger "${property}" has not been registered!`);
|
||||
}
|
||||
|
||||
let lookupRef = this._elementTriggerStates.get(element);
|
||||
if (!lookupRef) {
|
||||
this._elementTriggerStates.set(element, lookupRef = {});
|
||||
}
|
||||
|
||||
let oldValue = lookupRef.hasOwnProperty(property) ? lookupRef[property] : VOID_STATE;
|
||||
if (oldValue !== value) {
|
||||
value = normalizeTriggerValue(value);
|
||||
let instruction = trigger.matchTransition(oldValue, value);
|
||||
if (!instruction) {
|
||||
// we do this to make sure we always have an animation player so
|
||||
// that callback operations are properly called
|
||||
instruction = trigger.createFallbackInstruction(oldValue, value);
|
||||
}
|
||||
this.animateTransition(element, instruction);
|
||||
lookupRef[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => void {
|
||||
if (!eventPhase) {
|
||||
throw new Error(
|
||||
`Unable to listen on the animation trigger "${eventName}" because the provided event is undefined!`);
|
||||
}
|
||||
if (!this._triggers[eventName]) {
|
||||
throw new Error(
|
||||
`Unable to listen on the animation trigger event "${eventPhase}" because the animation trigger "${eventName}" doesn't exist!`);
|
||||
}
|
||||
let elementListeners = this._triggerListeners.get(element);
|
||||
if (!elementListeners) {
|
||||
this._triggerListeners.set(element, elementListeners = []);
|
||||
}
|
||||
validatePlayerEvent(eventName, eventPhase);
|
||||
const tuple = <TriggerListenerTuple>{triggerName: eventName, phase: eventPhase, callback};
|
||||
elementListeners.push(tuple);
|
||||
return () => {
|
||||
// this is queued up in the event that a removal animation is set
|
||||
// to fire on the element (the listeners need to be set during flush)
|
||||
getOrSetAsInMap(this._pendingListenerRemovals, element, []).push(tuple);
|
||||
};
|
||||
}
|
||||
|
||||
private _clearPendingListenerRemovals() {
|
||||
this._pendingListenerRemovals.forEach((tuples: TriggerListenerTuple[], element: any) => {
|
||||
const elementListeners = this._triggerListeners.get(element);
|
||||
if (elementListeners) {
|
||||
tuples.forEach(tuple => {
|
||||
const index = elementListeners.indexOf(tuple);
|
||||
if (index >= 0) {
|
||||
elementListeners.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this._pendingListenerRemovals.clear();
|
||||
}
|
||||
|
||||
private _onRemovalTransition(element: any): AnimationPlayer[] {
|
||||
// when a parent animation is set to trigger a removal we want to
|
||||
// find all of the children that are currently animating and clear
|
||||
// them out by destroying each of them.
|
||||
const elms = element.querySelectorAll(MARKED_FOR_ANIMATION_SELECTOR);
|
||||
for (let i = 0; i < elms.length; i++) {
|
||||
const elm = elms[i];
|
||||
const activePlayers = this._activeElementAnimations.get(elm);
|
||||
if (activePlayers) {
|
||||
activePlayers.forEach(player => player.destroy());
|
||||
}
|
||||
|
||||
const activeTransitions = this._activeTransitionAnimations.get(elm);
|
||||
if (activeTransitions) {
|
||||
Object.keys(activeTransitions).forEach(triggerName => {
|
||||
const player = activeTransitions[triggerName];
|
||||
if (player) {
|
||||
player.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// we make a copy of the array because the actual source array is modified
|
||||
// each time a player is finished/destroyed (the forEach loop would fail otherwise)
|
||||
return copyArray(this._activeElementAnimations.get(element) !);
|
||||
}
|
||||
|
||||
animateTransition(element: any, instruction: AnimationTransitionInstruction): AnimationPlayer {
|
||||
const triggerName = instruction.triggerName;
|
||||
|
||||
let previousPlayers: AnimationPlayer[];
|
||||
if (instruction.isRemovalTransition) {
|
||||
previousPlayers = this._onRemovalTransition(element);
|
||||
} else {
|
||||
previousPlayers = [];
|
||||
const existingTransitions = this._activeTransitionAnimations.get(element);
|
||||
const existingPlayer = existingTransitions ? existingTransitions[triggerName] : null;
|
||||
if (existingPlayer) {
|
||||
previousPlayers.push(existingPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
// it's important to do this step before destroying the players
|
||||
// so that the onDone callback below won't fire before this
|
||||
eraseStyles(element, instruction.fromStyles);
|
||||
|
||||
// we first run this so that the previous animation player
|
||||
// data can be passed into the successive animation players
|
||||
let totalTime = 0;
|
||||
const players = instruction.timelines.map((timelineInstruction, i) => {
|
||||
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
|
||||
return this._buildPlayer(element, timelineInstruction, previousPlayers, i);
|
||||
});
|
||||
|
||||
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
|
||||
const player = optimizeGroupPlayer(players);
|
||||
player.onDone(() => {
|
||||
player.destroy();
|
||||
const elmTransitionMap = this._activeTransitionAnimations.get(element);
|
||||
if (elmTransitionMap) {
|
||||
delete elmTransitionMap[triggerName];
|
||||
if (Object.keys(elmTransitionMap).length == 0) {
|
||||
this._activeTransitionAnimations.delete(element);
|
||||
}
|
||||
}
|
||||
deleteFromArrayMap(this._activeElementAnimations, element, player);
|
||||
setStyles(element, instruction.toStyles);
|
||||
});
|
||||
|
||||
const elmTransitionMap = getOrSetAsInMap(this._activeTransitionAnimations, element, {});
|
||||
elmTransitionMap[triggerName] = player;
|
||||
|
||||
this._queuePlayer(
|
||||
element, triggerName, player,
|
||||
makeAnimationEvent(
|
||||
element, triggerName, instruction.fromState, instruction.toState,
|
||||
null, // this will be filled in during event creation
|
||||
totalTime));
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
public animateTimeline(
|
||||
element: any, instructions: AnimationTimelineInstruction[],
|
||||
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
|
||||
const players = instructions.map((instruction, i) => {
|
||||
const player = this._buildPlayer(element, instruction, previousPlayers, i);
|
||||
player.onDestroy(
|
||||
() => { deleteFromArrayMap(this._activeElementAnimations, element, player); });
|
||||
this._markPlayerAsActive(element, player);
|
||||
return player;
|
||||
});
|
||||
return optimizeGroupPlayer(players);
|
||||
}
|
||||
|
||||
private _buildPlayer(
|
||||
element: any, instruction: AnimationTimelineInstruction, previousPlayers: AnimationPlayer[],
|
||||
index: number = 0): AnimationPlayer {
|
||||
// only the very first animation can absorb the previous styles. This
|
||||
// is here to prevent the an overlap situation where a group animation
|
||||
// absorbs previous styles multiple times for the same element.
|
||||
if (index && previousPlayers.length) {
|
||||
previousPlayers = [];
|
||||
}
|
||||
return this._driver.animate(
|
||||
element, this._normalizeKeyframes(instruction.keyframes), instruction.duration,
|
||||
instruction.delay, instruction.easing, previousPlayers);
|
||||
}
|
||||
|
||||
private _normalizeKeyframes(keyframes: ɵStyleData[]): ɵStyleData[] {
|
||||
const errors: string[] = [];
|
||||
const normalizedKeyframes: ɵStyleData[] = [];
|
||||
keyframes.forEach(kf => {
|
||||
const normalizedKeyframe: ɵStyleData = {};
|
||||
Object.keys(kf).forEach(prop => {
|
||||
let normalizedProp = prop;
|
||||
let normalizedValue = kf[prop];
|
||||
if (prop != 'offset') {
|
||||
normalizedProp = this._normalizer.normalizePropertyName(prop, errors);
|
||||
normalizedValue =
|
||||
this._normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
|
||||
}
|
||||
normalizedKeyframe[normalizedProp] = normalizedValue;
|
||||
});
|
||||
normalizedKeyframes.push(normalizedKeyframe);
|
||||
});
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
|
||||
}
|
||||
return normalizedKeyframes;
|
||||
}
|
||||
|
||||
private _markPlayerAsActive(element: any, player: AnimationPlayer) {
|
||||
const elementAnimations = getOrSetAsInMap(this._activeElementAnimations, element, []);
|
||||
elementAnimations.push(player);
|
||||
}
|
||||
|
||||
private _queuePlayer(
|
||||
element: any, triggerName: string, player: AnimationPlayer, event: AnimationEvent) {
|
||||
const tuple = <QueuedAnimationTransitionTuple>{element, player, triggerName, event};
|
||||
this._queuedTransitionAnimations.push(tuple);
|
||||
player.init();
|
||||
|
||||
element.classList.add(MARKED_FOR_ANIMATION_CLASSNAME);
|
||||
player.onDone(() => { element.classList.remove(MARKED_FOR_ANIMATION_CLASSNAME); });
|
||||
}
|
||||
|
||||
private _flushQueuedAnimations() {
|
||||
parentLoop: while (this._queuedTransitionAnimations.length) {
|
||||
const {player, element, triggerName, event} = this._queuedTransitionAnimations.shift() !;
|
||||
|
||||
let parent = element;
|
||||
while (parent = parent.parentNode) {
|
||||
// this means that a parent element will or will not
|
||||
// have its own animation operation which in this case
|
||||
// there's no point in even trying to do an animation
|
||||
if (parent[MARKED_FOR_REMOVAL]) continue parentLoop;
|
||||
}
|
||||
|
||||
const listeners = this._triggerListeners.get(element);
|
||||
if (listeners) {
|
||||
listeners.forEach(tuple => {
|
||||
if (tuple.triggerName == triggerName) {
|
||||
listenOnPlayer(player, tuple.phase, event, tuple.callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// if a removal exists for the given element then we need cancel
|
||||
// all the queued players so that a proper removal animation can go
|
||||
if (this._queuedRemovals.has(element)) {
|
||||
player.destroy();
|
||||
continue;
|
||||
}
|
||||
|
||||
this._markPlayerAsActive(element, player);
|
||||
|
||||
// in the event that an animation throws an error then we do
|
||||
// not want to re-run animations on any previous animations
|
||||
// if they have already been kicked off beforehand
|
||||
player.init();
|
||||
if (!player.hasStarted()) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
const leaveListeners = new Map<any, TriggerListenerTuple[]>();
|
||||
this._queuedRemovals.forEach((callback, element) => {
|
||||
const tuple = this._pendingListenerRemovals.get(element);
|
||||
if (tuple) {
|
||||
leaveListeners.set(element, tuple);
|
||||
this._pendingListenerRemovals.delete(element);
|
||||
}
|
||||
});
|
||||
|
||||
this._clearPendingListenerRemovals();
|
||||
this._pendingListenerRemovals = leaveListeners;
|
||||
|
||||
this._flushQueuedAnimations();
|
||||
|
||||
let flushAgain = false;
|
||||
this._queuedRemovals.forEach((callback, element) => {
|
||||
// an item that was inserted/removed in the same flush means
|
||||
// that an animation should not happen anyway
|
||||
if (this._flaggedInserts.has(element)) return;
|
||||
|
||||
let parent = element;
|
||||
let players: AnimationPlayer[] = [];
|
||||
while (parent = parent.parentNode) {
|
||||
// there is no reason to even try to
|
||||
if (parent[MARKED_FOR_REMOVAL]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const match = this._activeElementAnimations.get(parent);
|
||||
if (match) {
|
||||
players.push(...match);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// the loop was unable to find an parent that is animating even
|
||||
// though this element has set to be removed, so the algorithm
|
||||
// should check to see if there are any triggers on the element
|
||||
// that are present to handle a leave animation and then setup
|
||||
// those players to facilitate the callback after done
|
||||
if (players.length == 0) {
|
||||
// this means that the element has valid state triggers
|
||||
const stateDetails = this._elementTriggerStates.get(element);
|
||||
if (stateDetails) {
|
||||
Object.keys(stateDetails).forEach(triggerName => {
|
||||
flushAgain = true;
|
||||
const oldValue = stateDetails[triggerName];
|
||||
const instruction = this._triggers[triggerName].matchTransition(oldValue, VOID_STATE);
|
||||
if (instruction) {
|
||||
players.push(this.animateTransition(element, instruction));
|
||||
} else {
|
||||
const event = makeAnimationEvent(element, triggerName, oldValue, VOID_STATE, '', 0);
|
||||
const player = new NoopAnimationPlayer();
|
||||
this._queuePlayer(element, triggerName, player, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (players.length) {
|
||||
optimizeGroupPlayer(players).onDone(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
this._queuedRemovals.clear();
|
||||
this._flaggedInserts.clear();
|
||||
|
||||
// this means that one or more leave animations were detected
|
||||
if (flushAgain) {
|
||||
this._flushQueuedAnimations();
|
||||
this._clearPendingListenerRemovals();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getOrSetAsInMap(map: Map<any, any>, key: any, defaultValue: any) {
|
||||
let value = map.get(key);
|
||||
if (!value) {
|
||||
map.set(key, value = defaultValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function deleteFromArrayMap(map: Map<any, any[]>, key: any, value: any) {
|
||||
let arr = map.get(key);
|
||||
if (arr) {
|
||||
const index = arr.indexOf(value);
|
||||
if (index >= 0) {
|
||||
arr.splice(index, 1);
|
||||
if (arr.length == 0) {
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
|
||||
switch (players.length) {
|
||||
case 0:
|
||||
return new NoopAnimationPlayer();
|
||||
case 1:
|
||||
return players[0];
|
||||
default:
|
||||
return new ɵAnimationGroupPlayer(players);
|
||||
}
|
||||
}
|
||||
|
||||
function copyArray(source: any[]): any[] {
|
||||
return source ? source.splice(0) : [];
|
||||
}
|
||||
|
||||
function validatePlayerEvent(triggerName: string, eventName: string) {
|
||||
switch (eventName) {
|
||||
case 'start':
|
||||
case 'done':
|
||||
return;
|
||||
default:
|
||||
throw new Error(
|
||||
`The provided animation trigger event "${eventName}" for the animation trigger "${triggerName}" is not supported!`);
|
||||
}
|
||||
}
|
||||
|
||||
function listenOnPlayer(
|
||||
player: AnimationPlayer, eventName: string, baseEvent: AnimationEvent,
|
||||
callback: (event: any) => any) {
|
||||
switch (eventName) {
|
||||
case 'start':
|
||||
player.onStart(() => {
|
||||
const event = copyAnimationEvent(baseEvent);
|
||||
event.phaseName = 'start';
|
||||
callback(event);
|
||||
});
|
||||
break;
|
||||
case 'done':
|
||||
player.onDone(() => {
|
||||
const event = copyAnimationEvent(baseEvent);
|
||||
event.phaseName = 'done';
|
||||
callback(event);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function copyAnimationEvent(e: AnimationEvent): AnimationEvent {
|
||||
return makeAnimationEvent(
|
||||
e.element, e.triggerName, e.fromState, e.toState, e.phaseName, e.totalTime);
|
||||
}
|
||||
|
||||
function makeAnimationEvent(
|
||||
element: any, triggerName: string, fromState: string, toState: string, phaseName: string | null,
|
||||
totalTime: number): AnimationEvent {
|
||||
return <AnimationEvent>{element, triggerName, fromState, toState, phaseName, totalTime};
|
||||
}
|
||||
|
||||
function normalizeTriggerValue(value: any): string {
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
return value ? '1' : '0';
|
||||
default:
|
||||
return value ? value.toString() : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @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 {AnimationMetadata, AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
|
||||
|
||||
import {AnimationEngine} from '../animation_engine';
|
||||
import {TriggerAst} from '../dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../dsl/animation_ast_builder';
|
||||
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {parseTimelineCommand} from './shared';
|
||||
import {TimelineAnimationEngine} from './timeline_animation_engine';
|
||||
import {TransitionAnimationEngine} from './transition_animation_engine';
|
||||
|
||||
export class DomAnimationEngine implements AnimationEngine {
|
||||
private _transitionEngine: TransitionAnimationEngine;
|
||||
private _timelineEngine: TimelineAnimationEngine;
|
||||
|
||||
private _triggerCache: {[key: string]: AnimationTrigger} = {};
|
||||
|
||||
// this method is designed to be overridden by the code that uses this engine
|
||||
public onRemovalComplete = (element: any, context: any) => {};
|
||||
|
||||
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
|
||||
this._transitionEngine = new TransitionAnimationEngine(driver, normalizer);
|
||||
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
|
||||
|
||||
this._transitionEngine.onRemovalComplete =
|
||||
(element: any, context: any) => { this.onRemovalComplete(element, context); }
|
||||
}
|
||||
|
||||
registerTrigger(
|
||||
componentId: string, namespaceId: string, hostElement: any, name: string,
|
||||
metadata: AnimationTriggerMetadata): void {
|
||||
const cacheKey = componentId + '-' + name;
|
||||
let trigger = this._triggerCache[cacheKey];
|
||||
if (!trigger) {
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(metadata as AnimationMetadata, errors) as TriggerAst;
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
`The animation trigger "${name}" has failed to build due to the following errors:\n - ${errors.join("\n - ")}`);
|
||||
}
|
||||
trigger = buildTrigger(name, ast);
|
||||
this._triggerCache[cacheKey] = trigger;
|
||||
}
|
||||
|
||||
this._transitionEngine.register(namespaceId, hostElement, name, trigger);
|
||||
}
|
||||
|
||||
destroy(namespaceId: string, context: any) {
|
||||
this._transitionEngine.destroy(namespaceId, context);
|
||||
}
|
||||
|
||||
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {
|
||||
this._transitionEngine.insertNode(namespaceId, element, parent, insertBefore);
|
||||
}
|
||||
|
||||
onRemove(namespaceId: string, element: any, context: any): void {
|
||||
this._transitionEngine.removeNode(namespaceId, element, context);
|
||||
}
|
||||
|
||||
setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
|
||||
// @@property
|
||||
if (property.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(property);
|
||||
const args = value as any[];
|
||||
this._timelineEngine.command(id, element, action, args);
|
||||
return false;
|
||||
}
|
||||
return this._transitionEngine.trigger(namespaceId, element, property, value);
|
||||
}
|
||||
|
||||
listen(
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any {
|
||||
// @@listen
|
||||
if (eventName.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(eventName);
|
||||
return this._timelineEngine.listen(id, element, action, callback);
|
||||
}
|
||||
return this._transitionEngine.listen(namespaceId, element, eventName, eventPhase, callback);
|
||||
}
|
||||
|
||||
flush(): void { this._transitionEngine.flush(); }
|
||||
|
||||
get players(): AnimationPlayer[] {
|
||||
return (this._transitionEngine.players as AnimationPlayer[])
|
||||
.concat(this._timelineEngine.players as AnimationPlayer[]);
|
||||
}
|
||||
}
|
@ -5,20 +5,30 @@
|
||||
* 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 {AnimationEvent, AnimationMetadataType, AnimationPlayer, AnimationStateMetadata, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
|
||||
import {AnimationEvent, AnimationPlayer, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationEngine} from '../animation_engine';
|
||||
import {TriggerAst} from '../dsl/animation_ast';
|
||||
import {buildAnimationAst} from '../dsl/animation_ast_builder';
|
||||
import {buildTrigger} from '../dsl/animation_trigger';
|
||||
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
|
||||
import {copyStyles, eraseStyles, normalizeStyles, setStyles} from '../util';
|
||||
|
||||
import {AnimationDriver} from './animation_driver';
|
||||
import {parseTimelineCommand} from './shared';
|
||||
import {TimelineAnimationEngine} from './timeline_animation_engine';
|
||||
|
||||
interface ListenerTuple {
|
||||
eventPhase: string;
|
||||
triggerName: string;
|
||||
namespacedName: string;
|
||||
callback: (event: any) => any;
|
||||
doRemove?: boolean;
|
||||
}
|
||||
|
||||
interface ChangeTuple {
|
||||
element: any;
|
||||
namespacedName: string;
|
||||
triggerName: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
@ -35,36 +45,55 @@ export class NoopAnimationEngine extends AnimationEngine {
|
||||
private _triggerStyles: {[triggerName: string]: {[stateName: string]: ɵStyleData}} =
|
||||
Object.create(null);
|
||||
|
||||
registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void {
|
||||
name = name || trigger.name;
|
||||
private _timelineEngine: TimelineAnimationEngine;
|
||||
|
||||
// this method is designed to be overridden by the code that uses this engine
|
||||
public onRemovalComplete = (element: any, context: any) => {};
|
||||
|
||||
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
|
||||
super();
|
||||
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
|
||||
}
|
||||
|
||||
registerTrigger(
|
||||
componentId: string, namespaceId: string, hostElement: any, name: string,
|
||||
metadata: AnimationTriggerMetadata): void {
|
||||
name = name || metadata.name;
|
||||
name = namespaceId + '#' + name;
|
||||
if (this._triggerStyles[name]) {
|
||||
return;
|
||||
}
|
||||
const stateMap: {[stateName: string]: ɵStyleData} = {};
|
||||
trigger.definitions.forEach(def => {
|
||||
if (def.type === AnimationMetadataType.State) {
|
||||
const stateDef = def as AnimationStateMetadata;
|
||||
stateMap[stateDef.name] = normalizeStyles(stateDef.styles.styles);
|
||||
}
|
||||
});
|
||||
this._triggerStyles[name] = stateMap;
|
||||
|
||||
const errors: any[] = [];
|
||||
const ast = buildAnimationAst(metadata, errors) as TriggerAst;
|
||||
const trigger = buildTrigger(name, ast);
|
||||
this._triggerStyles[name] = trigger.states;
|
||||
}
|
||||
|
||||
onInsert(element: any, domFn: () => any): void { domFn(); }
|
||||
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {}
|
||||
|
||||
onRemove(element: any, domFn: () => any): void {
|
||||
domFn();
|
||||
onRemove(namespaceId: string, element: any, context: any): void {
|
||||
this.onRemovalComplete(element, context);
|
||||
if (element['nodeType'] == 1) {
|
||||
this._flaggedRemovals.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
setProperty(element: any, property: string, value: any): void {
|
||||
const storageProp = makeStorageProp(property);
|
||||
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
|
||||
this._changes.push(<ChangeTuple>{element, oldValue, newValue: value, triggerName: property});
|
||||
setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
|
||||
if (property.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(property);
|
||||
const args = value as any[];
|
||||
this._timelineEngine.command(id, element, action, args);
|
||||
return false;
|
||||
}
|
||||
|
||||
const triggerStateStyles = this._triggerStyles[property] || {};
|
||||
const namespacedName = namespaceId + '#' + property;
|
||||
const storageProp = makeStorageProp(namespacedName);
|
||||
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
|
||||
this._changes.push(
|
||||
<ChangeTuple>{element, oldValue, newValue: value, triggerName: property, namespacedName});
|
||||
|
||||
const triggerStateStyles = this._triggerStyles[namespacedName] || {};
|
||||
const fromStateStyles =
|
||||
triggerStateStyles[oldValue] || triggerStateStyles[DEFAULT_STATE_STYLES];
|
||||
if (fromStateStyles) {
|
||||
@ -78,16 +107,27 @@ export class NoopAnimationEngine extends AnimationEngine {
|
||||
setStyles(element, toStateStyles);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
|
||||
() => any {
|
||||
listen(
|
||||
namespaceId: string, element: any, eventName: string, eventPhase: string,
|
||||
callback: (event: any) => any): () => any {
|
||||
if (eventName.charAt(0) == '@') {
|
||||
const [id, action] = parseTimelineCommand(eventName);
|
||||
return this._timelineEngine.listen(id, element, action, callback);
|
||||
}
|
||||
|
||||
let listeners = this._listeners.get(element);
|
||||
if (!listeners) {
|
||||
this._listeners.set(element, listeners = []);
|
||||
}
|
||||
|
||||
const tuple = <ListenerTuple>{triggerName: eventName, eventPhase, callback};
|
||||
const tuple = <ListenerTuple>{
|
||||
namespacedName: namespaceId + '#' + eventName,
|
||||
triggerName: eventName, eventPhase, callback
|
||||
};
|
||||
listeners.push(tuple);
|
||||
|
||||
return () => tuple.doRemove = true;
|
||||
@ -113,7 +153,7 @@ export class NoopAnimationEngine extends AnimationEngine {
|
||||
const listeners = this._listeners.get(element);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
if (listener.triggerName == change.triggerName) {
|
||||
if (listener.namespacedName == change.namespacedName) {
|
||||
handleListener(listener, change);
|
||||
}
|
||||
});
|
||||
@ -126,10 +166,12 @@ export class NoopAnimationEngine extends AnimationEngine {
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
const triggerName = listener.triggerName;
|
||||
const storageProp = makeStorageProp(triggerName);
|
||||
const namespacedName = listener.namespacedName;
|
||||
const storageProp = makeStorageProp(namespacedName);
|
||||
handleListener(listener, <ChangeTuple>{
|
||||
element: element,
|
||||
triggerName: triggerName,
|
||||
element,
|
||||
triggerName,
|
||||
namespacedName: listener.namespacedName,
|
||||
oldValue: element[storageProp] || DEFAULT_STATE_VALUE,
|
||||
newValue: DEFAULT_STATE_VALUE
|
||||
});
|
||||
@ -156,8 +198,9 @@ export class NoopAnimationEngine extends AnimationEngine {
|
||||
this._onDoneFns = [];
|
||||
}
|
||||
|
||||
get activePlayers(): AnimationPlayer[] { return []; }
|
||||
get queuedPlayers(): AnimationPlayer[] { return []; }
|
||||
get players(): AnimationPlayer[] { return []; }
|
||||
|
||||
destroy(namespaceId: string) {}
|
||||
}
|
||||
|
||||
function makeAnimationEvent(
|
||||
|
116
packages/animations/browser/src/render/shared.ts
Normal file
116
packages/animations/browser/src/render/shared.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @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, AnimationEvent, AnimationPlayer, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
|
||||
|
||||
import {AnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
|
||||
import {AnimationDriver} from '../../src/render/animation_driver';
|
||||
|
||||
export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
|
||||
switch (players.length) {
|
||||
case 0:
|
||||
return new NoopAnimationPlayer();
|
||||
case 1:
|
||||
return players[0];
|
||||
default:
|
||||
return new ɵAnimationGroupPlayer(players);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeKeyframes(
|
||||
driver: AnimationDriver, normalizer: AnimationStyleNormalizer, element: any,
|
||||
keyframes: ɵStyleData[], preStyles: ɵStyleData = {},
|
||||
postStyles: ɵStyleData = {}): ɵStyleData[] {
|
||||
const errors: string[] = [];
|
||||
const normalizedKeyframes: ɵStyleData[] = [];
|
||||
let previousOffset = -1;
|
||||
let previousKeyframe: ɵStyleData|null = null;
|
||||
keyframes.forEach(kf => {
|
||||
const offset = kf['offset'] as number;
|
||||
const isSameOffset = offset == previousOffset;
|
||||
const normalizedKeyframe: ɵStyleData = (isSameOffset && previousKeyframe) || {};
|
||||
Object.keys(kf).forEach(prop => {
|
||||
let normalizedProp = prop;
|
||||
let normalizedValue = kf[prop];
|
||||
if (normalizedValue == PRE_STYLE) {
|
||||
normalizedValue = preStyles[prop];
|
||||
} else if (normalizedValue == AUTO_STYLE) {
|
||||
normalizedValue = postStyles[prop];
|
||||
} else if (prop != 'offset') {
|
||||
normalizedProp = normalizer.normalizePropertyName(prop, errors);
|
||||
normalizedValue = normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
|
||||
}
|
||||
normalizedKeyframe[normalizedProp] = normalizedValue;
|
||||
});
|
||||
if (!isSameOffset) {
|
||||
normalizedKeyframes.push(normalizedKeyframe);
|
||||
}
|
||||
previousKeyframe = normalizedKeyframe;
|
||||
previousOffset = offset;
|
||||
});
|
||||
if (errors.length) {
|
||||
const LINE_START = '\n - ';
|
||||
throw new Error(
|
||||
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
|
||||
}
|
||||
|
||||
return normalizedKeyframes;
|
||||
}
|
||||
|
||||
export function listenOnPlayer(
|
||||
player: AnimationPlayer, eventName: string, event: AnimationEvent | undefined,
|
||||
callback: (event: any) => any) {
|
||||
switch (eventName) {
|
||||
case 'start':
|
||||
player.onStart(() => callback(event && copyAnimationEvent(event, 'start', player.totalTime)));
|
||||
break;
|
||||
case 'done':
|
||||
player.onDone(() => callback(event && copyAnimationEvent(event, 'done', player.totalTime)));
|
||||
break;
|
||||
case 'destroy':
|
||||
player.onDestroy(
|
||||
() => callback(event && copyAnimationEvent(event, 'destroy', player.totalTime)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function copyAnimationEvent(
|
||||
e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent {
|
||||
return makeAnimationEvent(
|
||||
e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName,
|
||||
totalTime == undefined ? e.totalTime : totalTime);
|
||||
}
|
||||
|
||||
export function makeAnimationEvent(
|
||||
element: any, triggerName: string, fromState: string, toState: string, phaseName: string = '',
|
||||
totalTime: number = 0): AnimationEvent {
|
||||
return {element, triggerName, fromState, toState, phaseName, totalTime};
|
||||
}
|
||||
|
||||
export function getOrSetAsInMap(
|
||||
map: Map<any, any>| {[key: string]: any}, key: any, defaultValue: any) {
|
||||
let value: any;
|
||||
if (map instanceof Map) {
|
||||
value = map.get(key);
|
||||
if (!value) {
|
||||
map.set(key, value = defaultValue);
|
||||
}
|
||||
} else {
|
||||
value = map[key];
|
||||
if (!value) {
|
||||
value = map[key] = defaultValue;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseTimelineCommand(command: string): [string, string] {
|
||||
const separatorPos = command.indexOf(':');
|
||||
const id = command.substring(1, separatorPos);
|
||||
const action = command.substr(separatorPos + 1);
|
||||
return [id, action];
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @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<any, ɵStyleData>();
|
||||
|
||||
if (ast) {
|
||||
instructions =
|
||||
buildAnimationTimelines(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;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,15 @@ import {AnimationDriver} from '../animation_driver';
|
||||
import {WebAnimationsPlayer} from './web_animations_player';
|
||||
|
||||
export class WebAnimationsDriver implements AnimationDriver {
|
||||
computeStyle(element: any, prop: string, defaultValue?: string): string {
|
||||
return (window.getComputedStyle(element) as any)[prop] as string;
|
||||
}
|
||||
|
||||
animate(
|
||||
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
|
||||
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
|
||||
const playerOptions: {[key: string]: string |
|
||||
number} = {'duration': duration, 'delay': delay, 'fill': 'forwards'};
|
||||
const fill = delay == 0 ? 'both' : 'forwards';
|
||||
const playerOptions: {[key: string]: string | number} = {duration, delay, fill};
|
||||
|
||||
// we check for this to avoid having a null|undefined value be present
|
||||
// for the easing (which results in an error for certain browsers #9752)
|
||||
|
@ -5,7 +5,10 @@
|
||||
* 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, AnimationPlayer} from '@angular/animations';
|
||||
import {AnimationPlayer} from '@angular/animations';
|
||||
|
||||
import {copyStyles, eraseStyles, setStyles} from '../../util';
|
||||
|
||||
import {DOMAnimation} from './dom_animation';
|
||||
|
||||
export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
@ -24,18 +27,19 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
|
||||
public parentPlayer: AnimationPlayer|null = null;
|
||||
public previousStyles: {[styleName: string]: string | number};
|
||||
public currentSnapshot: {[styleName: string]: string | number} = {};
|
||||
|
||||
constructor(
|
||||
public element: any, public keyframes: {[key: string]: string | number}[],
|
||||
public options: {[key: string]: string | number},
|
||||
previousPlayers: WebAnimationsPlayer[] = []) {
|
||||
private previousPlayers: WebAnimationsPlayer[] = []) {
|
||||
this._duration = <number>options['duration'];
|
||||
this._delay = <number>options['delay'] || 0;
|
||||
this.time = this._duration + this._delay;
|
||||
|
||||
this.previousStyles = {};
|
||||
previousPlayers.forEach(player => {
|
||||
let styles = player._captureStyles();
|
||||
let styles = player.currentSnapshot;
|
||||
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
|
||||
});
|
||||
}
|
||||
@ -52,20 +56,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
|
||||
const keyframes = this.keyframes.map(styles => {
|
||||
const formattedKeyframe: {[key: string]: string | number} = {};
|
||||
Object.keys(styles).forEach((prop, index) => {
|
||||
let value = styles[prop];
|
||||
if (value == AUTO_STYLE) {
|
||||
value = _computeStyle(this.element, prop);
|
||||
}
|
||||
if (value != undefined) {
|
||||
formattedKeyframe[prop] = value;
|
||||
}
|
||||
});
|
||||
return formattedKeyframe;
|
||||
});
|
||||
|
||||
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
|
||||
const previousStyleProps = Object.keys(this.previousStyles);
|
||||
if (previousStyleProps.length) {
|
||||
let startingKeyframe = keyframes[0];
|
||||
@ -90,11 +81,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
}
|
||||
|
||||
this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
|
||||
this._finalKeyframe =
|
||||
keyframes.length ? _copyKeyframeStyles(keyframes[keyframes.length - 1]) : {};
|
||||
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
|
||||
|
||||
// this is required so that the player doesn't start to animate right away
|
||||
this._resetDomPlayerState();
|
||||
if (this._delay) {
|
||||
this._resetDomPlayerState();
|
||||
} else {
|
||||
this._player.pause();
|
||||
}
|
||||
this._player.addEventListener('finish', () => this._onFinish());
|
||||
}
|
||||
|
||||
@ -168,7 +162,9 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
|
||||
getPosition(): number { return this._player.currentTime / this.time; }
|
||||
|
||||
private _captureStyles(): {[prop: string]: string | number} {
|
||||
get totalTime(): number { return this._delay + this._duration; }
|
||||
|
||||
beforeDestroy() {
|
||||
const styles: {[key: string]: string | number} = {};
|
||||
if (this.hasStarted()) {
|
||||
Object.keys(this._finalKeyframe).forEach(prop => {
|
||||
@ -178,22 +174,10 @@ export class WebAnimationsPlayer implements AnimationPlayer {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return styles;
|
||||
this.currentSnapshot = styles;
|
||||
}
|
||||
}
|
||||
|
||||
function _computeStyle(element: any, prop: string): string {
|
||||
return (<any>window.getComputedStyle(element))[prop];
|
||||
}
|
||||
|
||||
function _copyKeyframeStyles(styles: {[style: string]: string | number}):
|
||||
{[style: string]: string | number} {
|
||||
const newStyles: {[style: string]: string | number} = {};
|
||||
Object.keys(styles).forEach(prop => {
|
||||
if (prop != 'offset') {
|
||||
newStyles[prop] = styles[prop];
|
||||
}
|
||||
});
|
||||
return newStyles;
|
||||
}
|
||||
|
@ -5,37 +5,62 @@
|
||||
* 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 {AnimateTimings, ɵStyleData} from '@angular/animations';
|
||||
import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleData} from '@angular/animations';
|
||||
|
||||
export const ONE_SECOND = 1000;
|
||||
|
||||
export function parseTimeExpression(exp: string | number, errors: string[]): AnimateTimings {
|
||||
const regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
|
||||
export const ENTER_CLASSNAME = 'ng-enter';
|
||||
export const LEAVE_CLASSNAME = 'ng-leave';
|
||||
export const ENTER_SELECTOR = '.ng-enter';
|
||||
export const LEAVE_SELECTOR = '.ng-leave';
|
||||
export const NG_TRIGGER_CLASSNAME = 'ng-trigger';
|
||||
export const NG_TRIGGER_SELECTOR = '.ng-trigger';
|
||||
export const NG_ANIMATING_CLASSNAME = 'ng-animating';
|
||||
export const NG_ANIMATING_SELECTOR = '.ng-animating';
|
||||
|
||||
export function resolveTimingValue(value: string | number) {
|
||||
if (typeof value == 'number') return value;
|
||||
|
||||
const matches = (value as string).match(/^(-?[\.\d]+)(m?s)/);
|
||||
if (!matches || matches.length < 2) return 0;
|
||||
|
||||
return _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
|
||||
}
|
||||
|
||||
function _convertTimeValueToMS(value: number, unit: string): number {
|
||||
switch (unit) {
|
||||
case 's':
|
||||
return value * ONE_SECOND;
|
||||
default: // ms or something else
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTiming(
|
||||
timings: string | number | AnimateTimings, errors: any[], allowNegativeValues?: boolean) {
|
||||
return timings.hasOwnProperty('duration') ?
|
||||
<AnimateTimings>timings :
|
||||
parseTimeExpression(<string|number>timings, errors, allowNegativeValues);
|
||||
}
|
||||
|
||||
function parseTimeExpression(
|
||||
exp: string | number, errors: string[], allowNegativeValues?: boolean): AnimateTimings {
|
||||
const regex = /^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
|
||||
let duration: number;
|
||||
let delay: number = 0;
|
||||
let easing: string|null = null;
|
||||
let easing: string = '';
|
||||
if (typeof exp === 'string') {
|
||||
const matches = exp.match(regex);
|
||||
if (matches === null) {
|
||||
errors.push(`The provided timing value "${exp}" is invalid.`);
|
||||
return {duration: 0, delay: 0, easing: null};
|
||||
return {duration: 0, delay: 0, easing: ''};
|
||||
}
|
||||
|
||||
let durationMatch = parseFloat(matches[1]);
|
||||
const durationUnit = matches[2];
|
||||
if (durationUnit == 's') {
|
||||
durationMatch *= ONE_SECOND;
|
||||
}
|
||||
duration = Math.floor(durationMatch);
|
||||
duration = _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
|
||||
|
||||
const delayMatch = matches[3];
|
||||
const delayUnit = matches[4];
|
||||
if (delayMatch != null) {
|
||||
let delayVal: number = parseFloat(delayMatch);
|
||||
if (delayUnit != null && delayUnit == 's') {
|
||||
delayVal *= ONE_SECOND;
|
||||
}
|
||||
delay = Math.floor(delayVal);
|
||||
delay = _convertTimeValueToMS(Math.floor(parseFloat(delayMatch)), matches[4]);
|
||||
}
|
||||
|
||||
const easingVal = matches[5];
|
||||
@ -46,9 +71,31 @@ export function parseTimeExpression(exp: string | number, errors: string[]): Ani
|
||||
duration = <number>exp;
|
||||
}
|
||||
|
||||
if (!allowNegativeValues) {
|
||||
let containsErrors = false;
|
||||
let startIndex = errors.length;
|
||||
if (duration < 0) {
|
||||
errors.push(`Duration values below 0 are not allowed for this animation step.`);
|
||||
containsErrors = true;
|
||||
}
|
||||
if (delay < 0) {
|
||||
errors.push(`Delay values below 0 are not allowed for this animation step.`);
|
||||
containsErrors = true;
|
||||
}
|
||||
if (containsErrors) {
|
||||
errors.splice(startIndex, 0, `The provided timing value "${exp}" is invalid.`);
|
||||
}
|
||||
}
|
||||
|
||||
return {duration, delay, easing};
|
||||
}
|
||||
|
||||
export function copyObj(
|
||||
obj: {[key: string]: any}, destination: {[key: string]: any} = {}): {[key: string]: any} {
|
||||
Object.keys(obj).forEach(prop => { destination[prop] = obj[prop]; });
|
||||
return destination;
|
||||
}
|
||||
|
||||
export function normalizeStyles(styles: ɵStyleData | ɵStyleData[]): ɵStyleData {
|
||||
const normalizedStyles: ɵStyleData = {};
|
||||
if (Array.isArray(styles)) {
|
||||
@ -69,7 +116,7 @@ export function copyStyles(
|
||||
destination[prop] = styles[prop];
|
||||
}
|
||||
} else {
|
||||
Object.keys(styles).forEach(prop => destination[prop] = styles[prop]);
|
||||
copyObj(styles, destination);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
@ -89,3 +136,73 @@ export function eraseStyles(element: any, styles: ɵStyleData) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMetadata[]):
|
||||
AnimationMetadata {
|
||||
if (Array.isArray(steps)) {
|
||||
if (steps.length == 1) return steps[0];
|
||||
return sequence(steps);
|
||||
}
|
||||
return steps as AnimationMetadata;
|
||||
}
|
||||
|
||||
export function validateStyleParams(
|
||||
value: string | number, options: AnimationOptions, errors: any[]) {
|
||||
const params = options.params || {};
|
||||
if (typeof value !== 'string') return;
|
||||
|
||||
const matches = value.toString().match(PARAM_REGEX);
|
||||
if (matches) {
|
||||
matches.forEach(varName => {
|
||||
if (!params.hasOwnProperty(varName)) {
|
||||
errors.push(
|
||||
`Unable to resolve the local animation param ${varName} in the given list of values`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
|
||||
export function interpolateParams(
|
||||
value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
|
||||
const original = value.toString();
|
||||
const str = original.replace(PARAM_REGEX, (_, varName) => {
|
||||
let localVal = params[varName];
|
||||
// this means that the value was never overidden by the data passed in by the user
|
||||
if (!params.hasOwnProperty(varName)) {
|
||||
errors.push(`Please provide a value for the animation param ${varName}`);
|
||||
localVal = '';
|
||||
}
|
||||
return localVal.toString();
|
||||
});
|
||||
|
||||
// we do this to assert that numeric values stay as they are
|
||||
return str == original ? value : str;
|
||||
}
|
||||
|
||||
export function iteratorToArray(iterator: any): any[] {
|
||||
const arr: any[] = [];
|
||||
let item = iterator.next();
|
||||
while (!item.done) {
|
||||
arr.push(item.value);
|
||||
item = iterator.next();
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function mergeAnimationOptions(
|
||||
source: AnimationOptions, destination: AnimationOptions): AnimationOptions {
|
||||
if (source.params) {
|
||||
const p0 = source.params;
|
||||
if (!destination.params) {
|
||||
destination.params = {};
|
||||
}
|
||||
const p1 = destination.params;
|
||||
Object.keys(p0).forEach(param => {
|
||||
if (!p1.hasOwnProperty(param)) {
|
||||
p1[param] = p0[param];
|
||||
}
|
||||
});
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
Reference in New Issue
Block a user