refactor(animations): do not inherit past styles into sub timelines

This commit is contained in:
Matias Niemelä
2017-02-10 14:22:34 -08:00
committed by Igor Minar
parent 3dbd9a04d4
commit 88e3d7af9f
3 changed files with 96 additions and 78 deletions

View File

@ -51,17 +51,10 @@ import {AnimationTimelineInstruction, createTimelineInstruction} from './animati
*
* [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. There is always one top-level timeline and sub timelines
* are forked in two specific cases:
*
* 1. When keyframes() is used it will create a sub timeline. Upon creation, ALL OF THE COLLECTED
* STYLES from the parent timeline up until this point will be inherited into the keyframes
* timeline.
*
* 2. When group() is used it will create a sub timeline. Upon creation, NONE OF THE COLLECTED
* STYLES from the parent timeline will be inherited. Although, if the sub timeline does reference a
* style that was previously used within the parent then it will be copied over into the sub
* timeline.
* 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
@ -96,22 +89,14 @@ import {AnimationTimelineInstruction, createTimelineInstruction} from './animati
* 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.)
*
* Based on whether the styles are inherited into a sub timeline (depending on the two cases
* mentioned above), the functionality of the backFill will behave differently:
*
* 1. If the styles are inherited from the parent then the backFill property will also be inherited
* and therefore any newly added styles to the backFill will be propagated to the parent timeline
* and its already processed keyframes.
*
* 2. If the styles are not inherited from the parent then the sub timeline will have its own
* backFill. Then if the sub timeline comes across a property that was not defined already then it
* will read that from the parent's styles and pass that into its own backFill (which will then
* propagate the missing styles across the sub timeline only).
*
* [Validation]
* The code in this file is not responsible for validation. That functionaliy happens with within
* The code in this file is not responsible for validation. That functionality happens with within
* the `AnimationValidatorVisitor` code.
*/
export function buildAnimationKeyframes(
@ -139,9 +124,9 @@ export class AnimationTimelineContext {
timelines.push(this.currentTimeline);
}
createSubContext(inherit: boolean = false): AnimationTimelineContext {
const context = new AnimationTimelineContext(
this.errors, this.timelines, this.currentTimeline.fork(inherit));
createSubContext(): AnimationTimelineContext {
const context =
new AnimationTimelineContext(this.errors, this.timelines, this.currentTimeline.fork());
context.previousNode = this.previousNode;
context.currentAnimateTimings = this.currentAnimateTimings;
this.subContextCount++;
@ -154,7 +139,7 @@ export class AnimationTimelineContext {
if (newTime > 0) {
oldTimeline.time = newTime;
}
this.currentTimeline = oldTimeline.fork(true);
this.currentTimeline = oldTimeline.fork();
oldTimeline.time = oldTime;
this.timelines.push(this.currentTimeline);
return this.currentTimeline;
@ -217,35 +202,33 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
context.currentTimeline.snapshotCurrentStyles();
}
ast.steps.map(s => visitAnimationNode(this, s, context));
context.previousNode = ast;
// 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.currentTimeline.snapshotCurrentStyles();
}
context.previousNode = ast;
}
visitGroup(ast: meta.AnimationGroupMetadata, context: AnimationTimelineContext) {
const innerTimelines: TimelineBuilder[] = [];
let furthestTime = context.currentTimeline.currentTime;
ast.steps.map(s => {
const innerContext = context.createSubContext(false);
innerContext.currentTimeline.snapshotCurrentStyles();
const innerContext = context.createSubContext();
visitAnimationNode(this, s, innerContext);
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
innerTimelines.push(innerContext.currentTimeline);
});
context.transformIntoNewTimeline(furthestTime);
// 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));
// 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.currentTimeline.snapshotCurrentStyles();
context.transformIntoNewTimeline(furthestTime);
context.previousNode = ast;
}
@ -307,16 +290,10 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
}
const keyframeDuration = context.currentAnimateTimings.duration;
const innerContext = context.createSubContext(true);
const innerContext = context.createSubContext();
const innerTimeline = innerContext.currentTimeline;
innerTimeline.easing = context.currentAnimateTimings.easing;
// this will ensure that all collected styles so far
// are populated into the first keyframe of the keyframes()
// timeline (even if there exists a starting keyframe then
// it will override the contents of the first frame later)
innerTimeline.snapshotCurrentStyles();
ast.steps.map((step: meta.AnimationStyleMetadata, i: number) => {
const normalizedStyles = normalizeStyles(new AnimationStyles(step.styles));
const offset = containsOffsets ? <number>normalizedStyles['offset'] :
@ -332,7 +309,6 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
// 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(context.currentTimeline.time + keyframeDuration);
context.currentTimeline.snapshotCurrentStyles();
context.previousNode = ast;
}
}
@ -346,18 +322,8 @@ export class TimelineBuilder {
private _localTimelineStyles: StyleData;
private _backFill: StyleData = {};
constructor(
public startTime: number, private _globalTimelineStyles: StyleData = null,
inheritedBackFill: StyleData = null, inheritedStyles: StyleData = null) {
if (inheritedBackFill) {
this._backFill = inheritedBackFill;
}
constructor(public startTime: number, private _globalTimelineStyles: StyleData = null) {
this._localTimelineStyles = Object.create(this._backFill, {});
if (inheritedStyles) {
this._localTimelineStyles = copyStyles(inheritedStyles, false, this._localTimelineStyles);
}
if (!this._globalTimelineStyles) {
this._globalTimelineStyles = this._localTimelineStyles;
}
@ -368,11 +334,8 @@ export class TimelineBuilder {
get currentTime() { return this.startTime + this.time; }
fork(inherit: boolean = false): TimelineBuilder {
let inheritedBackFill = inherit ? this._backFill : null;
let inheritedStyles = inherit ? this._localTimelineStyles : null;
return new TimelineBuilder(
this.currentTime, this._globalTimelineStyles, inheritedBackFill, inheritedStyles);
fork(): TimelineBuilder {
return new TimelineBuilder(this.currentTime, this._globalTimelineStyles);
}
private _loadKeyframe() {
@ -395,9 +358,6 @@ export class TimelineBuilder {
private _updateStyle(prop: string, value: string|number) {
if (prop != 'easing') {
if (!this._localTimelineStyles[prop]) {
this._backFill[prop] = this._globalTimelineStyles[prop] || meta.AUTO_STYLE;
}
this._localTimelineStyles[prop] = value;
this._globalTimelineStyles[prop] = value;
this._styleSummary[prop] = {time: this.currentTime, value};
@ -409,6 +369,9 @@ export class TimelineBuilder {
if (prop !== 'offset') {
const val = styles[prop];
this._currentKeyframe[prop] = val;
if (prop !== 'easing' && !this._localTimelineStyles[prop]) {
this._backFill[prop] = this._globalTimelineStyles[prop] || meta.AUTO_STYLE;
}
this._updateStyle(prop, val);
}
});

View File

@ -16,11 +16,13 @@ export class WebAnimationsPlayer implements AnimationPlayer {
private _onDestroyFns: Function[] = [];
private _player: DOMAnimation;
private _duration: number;
private _delay: number;
private _initialized = false;
private _finished = false;
private _started = false;
private _destroyed = false;
private _finalKeyframe: {[key: string]: string | number};
public time = 0;
public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number};
@ -30,6 +32,8 @@ export class WebAnimationsPlayer implements AnimationPlayer {
public options: {[key: string]: string | number},
previousPlayers: WebAnimationsPlayer[] = []) {
this._duration = <number>options['duration'];
this._delay = <number>options['delay'] || 0;
this.time = this._duration + this._delay;
this.previousStyles = {};
previousPlayers.forEach(player => {
@ -157,11 +161,9 @@ export class WebAnimationsPlayer implements AnimationPlayer {
}
}
get totalTime(): number { return this._duration; }
setPosition(p: number): void { this._player.currentTime = p * this.time; }
setPosition(p: number): void { this._player.currentTime = p * this.totalTime; }
getPosition(): number { return this._player.currentTime / this.totalTime; }
getPosition(): number { return this._player.currentTime / this.time; }
private _captureStyles(): {[prop: string]: string | number} {
const styles: {[key: string]: string | number} = {};