diff --git a/src/animation.js b/src/animation.js index f1d21d8..e9f6482 100644 --- a/src/animation.js +++ b/src/animation.js @@ -94,7 +94,7 @@ this._paused = true; } this._tickCurrentTime(newTime, true); - scope.invalidateEffects(); + scope.applyDirtiedAnimation(this); }, get startTime() { return this._startTime; @@ -107,7 +107,7 @@ return; this._startTime = newTime; this._tickCurrentTime((this._timeline.currentTime - this._startTime) * this.playbackRate); - scope.invalidateEffects(); + scope.applyDirtiedAnimation(this); }, get playbackRate() { return this._playbackRate; @@ -123,7 +123,7 @@ this._finishedFlag = false; this._idle = false; this._ensureAlive(); - scope.invalidateEffects(); + scope.applyDirtiedAnimation(this); } if (oldCurrentTime != null) { this.currentTime = oldCurrentTime; @@ -165,7 +165,7 @@ this._finishedFlag = false; this._idle = false; this._ensureAlive(); - scope.invalidateEffects(); + scope.applyDirtiedAnimation(this); }, pause: function() { if (!this._isFinished && !this._paused && !this._idle) { @@ -183,7 +183,7 @@ this.currentTime = this._playbackRate > 0 ? this._totalDuration : 0; this._startTime = this._totalDuration - this.currentTime; this._currentTimePending = false; - scope.invalidateEffects(); + scope.applyDirtiedAnimation(this); }, cancel: function() { if (!this._inEffect) @@ -191,13 +191,14 @@ this._inEffect = false; this._idle = true; this._paused = false; + this._isFinished = true; this._finishedFlag = true; this._currentTime = 0; this._startTime = null; this._effect._update(null); // effects are invalid after cancellation as the animation state // needs to un-apply. - scope.invalidateEffects(); + scope.applyDirtiedAnimation(this); }, reverse: function() { this.playbackRate *= -1; @@ -249,6 +250,26 @@ get _needsTick() { return (this.playState in {'pending': 1, 'running': 1}) || !this._finishedFlag; }, + _targetAnimations: function() { + var target = this._effect._target; + if (!target._activeAnimations) { + target._activeAnimations = []; + } + return target._activeAnimations; + }, + _markTarget: function() { + var animations = this._targetAnimations(); + if (animations.indexOf(this) === -1) { + animations.push(this); + } + }, + _unmarkTarget: function() { + var animations = this._targetAnimations(); + var index = animations.indexOf(this); + if (index !== -1) { + animations.splice(index, 1); + } + }, }; if (WEB_ANIMATIONS_TESTING) { diff --git a/src/keyframe-effect.js b/src/keyframe-effect.js index c79f05d..f6813d8 100644 --- a/src/keyframe-effect.js +++ b/src/keyframe-effect.js @@ -43,6 +43,7 @@ keyframeEffect._hasSameTarget = function(otherTarget) { return target === otherTarget; }; + keyframeEffect._target = target; keyframeEffect._totalDuration = effectTime._totalDuration; keyframeEffect._id = id; return keyframeEffect; diff --git a/src/tick.js b/src/tick.js index 3112f6b..08b0a14 100644 --- a/src/tick.js +++ b/src/tick.js @@ -39,7 +39,8 @@ rafCallbacks = []; if (t < timeline.currentTime) t = timeline.currentTime; - tick(t, true); + timeline._animations.sort(compareAnimations); + timeline._animations = tick(t, true, timeline._animations)[0]; processing.forEach(function(entry) { entry[1](t); }); applyPendingEffects(); _now = undefined; @@ -63,7 +64,7 @@ animation._timeline = this; this._animations.push(animation); scope.restart(); - scope.invalidateEffects(); + scope.applyDirtiedAnimation(animation); return animation; } }; @@ -92,8 +93,24 @@ return hasRestartedThisFrame; }; - scope.invalidateEffects = function() { - tick(scope.timeline.currentTime, false); + // RAF is supposed to be the last script to occur before frame rendering but not + // all browsers behave like this. This function is for synchonously updating an + // animation's effects whenever its state is mutated by script to work around + // incorrect script execution ordering by the browser. + scope.applyDirtiedAnimation = function(animation) { + if (inTick) { + return; + } + animation._markTarget(); + var animations = animation._targetAnimations(); + animations.sort(compareAnimations); + var inactiveAnimations = tick(scope.timeline.currentTime, false, animations.slice())[1]; + inactiveAnimations.forEach(function(animation) { + var index = timeline._animations.indexOf(animation); + if (index !== -1) { + timeline._animations.splice(index, 1); + } + }); applyPendingEffects(); }; @@ -105,41 +122,51 @@ var t60hz = 1000 / 60; - function tick(t, isAnimationFrame) { + var inTick = false; + function tick(t, isAnimationFrame, updatingAnimations) { + inTick = true; hasRestartedThisFrame = false; var timeline = scope.timeline; + timeline.currentTime = t; - timeline._animations.sort(compareAnimations); ticking = false; - var updatingAnimations = timeline._animations; - timeline._animations = []; var newPendingClears = []; var newPendingEffects = []; - updatingAnimations = updatingAnimations.filter(function(animation) { + var activeAnimations = []; + var inactiveAnimations = []; + updatingAnimations.forEach(function(animation) { animation._tick(t, isAnimationFrame); - if (!animation._inEffect) + if (!animation._inEffect) { newPendingClears.push(animation._effect); - else + animation._unmarkTarget(); + } else { newPendingEffects.push(animation._effect); + animation._markTarget(); + } if (animation._needsTick) ticking = true; var alive = animation._inEffect || animation._needsTick; animation._inTimeline = alive; - return alive; + if (alive) { + activeAnimations.push(animation); + } else { + inactiveAnimations.push(animation); + } }); // FIXME: Should remove dupliactes from pendingEffects. pendingEffects.push.apply(pendingEffects, newPendingClears); pendingEffects.push.apply(pendingEffects, newPendingEffects); - timeline._animations.push.apply(timeline._animations, updatingAnimations); - if (ticking) requestAnimationFrame(function() {}); + + inTick = false; + return [activeAnimations, inactiveAnimations]; }; if (WEB_ANIMATIONS_TESTING) {