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

committed by
Jason Aden

parent
d761059e4d
commit
16c8167886
@ -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(); }
|
||||
}
|
Reference in New Issue
Block a user