// Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. (function(shared, scope, testing) { scope.animationsWithPromises = []; scope.Animation = function(effect, timeline) { this.id = ''; if (effect && effect._id) { this.id = effect._id; } this.effect = effect; if (effect) { effect._animation = this; } if (!timeline) { throw new Error('Animation with null timeline is not supported'); } this._timeline = timeline; this._sequenceNumber = shared.sequenceNumber++; this._holdTime = 0; this._paused = false; this._isGroup = false; this._animation = null; this._childAnimations = []; this._callback = null; this._oldPlayState = 'idle'; this._rebuildUnderlyingAnimation(); // Animations are constructed in the idle state. this._animation.cancel(); this._updatePromises(); }; scope.Animation.prototype = { _updatePromises: function() { var oldPlayState = this._oldPlayState; var newPlayState = this.playState; if (this._readyPromise && newPlayState !== oldPlayState) { if (newPlayState == 'idle') { this._rejectReadyPromise(); this._readyPromise = undefined; } else if (oldPlayState == 'pending') { this._resolveReadyPromise(); } else if (newPlayState == 'pending') { this._readyPromise = undefined; } } if (this._finishedPromise && newPlayState !== oldPlayState) { if (newPlayState == 'idle') { this._rejectFinishedPromise(); this._finishedPromise = undefined; } else if (newPlayState == 'finished') { this._resolveFinishedPromise(); } else if (oldPlayState == 'finished') { this._finishedPromise = undefined; } } this._oldPlayState = this.playState; return (this._readyPromise || this._finishedPromise); }, _rebuildUnderlyingAnimation: function() { this._updatePromises(); var oldPlaybackRate; var oldPaused; var oldStartTime; var oldCurrentTime; var hadUnderlying = this._animation ? true : false; if (hadUnderlying) { oldPlaybackRate = this.playbackRate; oldPaused = this._paused; oldStartTime = this.startTime; oldCurrentTime = this.currentTime; this._animation.cancel(); this._animation._wrapper = null; this._animation = null; } if (!this.effect || this.effect instanceof window.KeyframeEffect) { this._animation = scope.newUnderlyingAnimationForKeyframeEffect(this.effect); scope.bindAnimationForKeyframeEffect(this); } if (this.effect instanceof window.SequenceEffect || this.effect instanceof window.GroupEffect) { this._animation = scope.newUnderlyingAnimationForGroup(this.effect); scope.bindAnimationForGroup(this); } if (this.effect && this.effect._onsample) { scope.bindAnimationForCustomEffect(this); } if (hadUnderlying) { if (oldPlaybackRate != 1) { this.playbackRate = oldPlaybackRate; } if (oldStartTime !== null) { this.startTime = oldStartTime; } else if (oldCurrentTime !== null) { this.currentTime = oldCurrentTime; } else if (this._holdTime !== null) { this.currentTime = this._holdTime; } if (oldPaused) { this.pause(); } } this._updatePromises(); }, _updateChildren: function() { if (!this.effect || this.playState == 'idle') return; var offset = this.effect._timing.delay; this._childAnimations.forEach(function(childAnimation) { this._arrangeChildren(childAnimation, offset); if (this.effect instanceof window.SequenceEffect) offset += scope.groupChildDuration(childAnimation.effect); }.bind(this)); }, _setExternalAnimation: function(animation) { if (!this.effect || !this._isGroup) return; for (var i = 0; i < this.effect.children.length; i++) { this.effect.children[i]._animation = animation; this._childAnimations[i]._setExternalAnimation(animation); } }, _constructChildAnimations: function() { if (!this.effect || !this._isGroup) return; var offset = this.effect._timing.delay; this._removeChildAnimations(); this.effect.children.forEach(function(child) { var childAnimation = scope.timeline._play(child); this._childAnimations.push(childAnimation); childAnimation.playbackRate = this.playbackRate; if (this._paused) childAnimation.pause(); child._animation = this.effect._animation; this._arrangeChildren(childAnimation, offset); if (this.effect instanceof window.SequenceEffect) offset += scope.groupChildDuration(child); }.bind(this)); }, _arrangeChildren: function(childAnimation, offset) { if (this.startTime === null) { childAnimation.currentTime = this.currentTime - offset / this.playbackRate; } else if (childAnimation.startTime !== this.startTime + offset / this.playbackRate) { childAnimation.startTime = this.startTime + offset / this.playbackRate; } }, get timeline() { return this._timeline; }, get playState() { return this._animation ? this._animation.playState : 'idle'; }, get finished() { if (!window.Promise) { console.warn('Animation Promises require JavaScript Promise constructor'); return null; } if (!this._finishedPromise) { if (scope.animationsWithPromises.indexOf(this) == -1) { scope.animationsWithPromises.push(this); } this._finishedPromise = new Promise( function(resolve, reject) { this._resolveFinishedPromise = function() { resolve(this); }; this._rejectFinishedPromise = function() { reject({type: DOMException.ABORT_ERR, name: 'AbortError'}); }; }.bind(this)); if (this.playState == 'finished') { this._resolveFinishedPromise(); } } return this._finishedPromise; }, get ready() { if (!window.Promise) { console.warn('Animation Promises require JavaScript Promise constructor'); return null; } if (!this._readyPromise) { if (scope.animationsWithPromises.indexOf(this) == -1) { scope.animationsWithPromises.push(this); } this._readyPromise = new Promise( function(resolve, reject) { this._resolveReadyPromise = function() { resolve(this); }; this._rejectReadyPromise = function() { reject({type: DOMException.ABORT_ERR, name: 'AbortError'}); }; }.bind(this)); if (this.playState !== 'pending') { this._resolveReadyPromise(); } } return this._readyPromise; }, get onfinish() { return this._animation.onfinish; }, set onfinish(v) { if (typeof v == 'function') { this._animation.onfinish = (function(e) { e.target = this; v.call(this, e); }).bind(this); } else { this._animation.onfinish = v; } }, get oncancel() { return this._animation.oncancel; }, set oncancel(v) { if (typeof v == 'function') { this._animation.oncancel = (function(e) { e.target = this; v.call(this, e); }).bind(this); } else { this._animation.oncancel = v; } }, get currentTime() { this._updatePromises(); var currentTime = this._animation.currentTime; this._updatePromises(); return currentTime; }, set currentTime(v) { this._updatePromises(); this._animation.currentTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE; this._register(); this._forEachChild(function(child, offset) { child.currentTime = v - offset; }); this._updatePromises(); }, get startTime() { return this._animation.startTime; }, set startTime(v) { this._updatePromises(); this._animation.startTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE; this._register(); this._forEachChild(function(child, offset) { child.startTime = v + offset; }); this._updatePromises(); }, get playbackRate() { return this._animation.playbackRate; }, set playbackRate(value) { this._updatePromises(); var oldCurrentTime = this.currentTime; this._animation.playbackRate = value; this._forEachChild(function(childAnimation) { childAnimation.playbackRate = value; }); if (oldCurrentTime !== null) { this.currentTime = oldCurrentTime; } this._updatePromises(); }, play: function() { this._updatePromises(); this._paused = false; this._animation.play(); if (this._timeline._animations.indexOf(this) == -1) { this._timeline._animations.push(this); } this._register(); scope.awaitStartTime(this); this._forEachChild(function(child) { var time = child.currentTime; child.play(); child.currentTime = time; }); this._updatePromises(); }, pause: function() { this._updatePromises(); if (this.currentTime) { this._holdTime = this.currentTime; } this._animation.pause(); this._register(); this._forEachChild(function(child) { child.pause(); }); this._paused = true; this._updatePromises(); }, finish: function() { this._updatePromises(); this._animation.finish(); this._register(); this._updatePromises(); }, cancel: function() { this._updatePromises(); this._animation.cancel(); this._register(); this._removeChildAnimations(); this._updatePromises(); }, reverse: function() { this._updatePromises(); var oldCurrentTime = this.currentTime; this._animation.reverse(); this._forEachChild(function(childAnimation) { childAnimation.reverse(); }); if (oldCurrentTime !== null) { this.currentTime = oldCurrentTime; } this._updatePromises(); }, addEventListener: function(type, handler) { var wrapped = handler; if (typeof handler == 'function') { wrapped = (function(e) { e.target = this; handler.call(this, e); }).bind(this); handler._wrapper = wrapped; } this._animation.addEventListener(type, wrapped); }, removeEventListener: function(type, handler) { this._animation.removeEventListener(type, (handler && handler._wrapper) || handler); }, _removeChildAnimations: function() { while (this._childAnimations.length) this._childAnimations.pop().cancel(); }, _forEachChild: function(f) { var offset = 0; if (this.effect.children && this._childAnimations.length < this.effect.children.length) this._constructChildAnimations(); this._childAnimations.forEach(function(child) { f.call(this, child, offset); if (this.effect instanceof window.SequenceEffect) offset += child.effect.activeDuration; }.bind(this)); if (this.playState == 'pending') return; var timing = this.effect._timing; var t = this.currentTime; if (t !== null) t = shared.calculateIterationProgress(shared.calculateActiveDuration(timing), t, timing); if (t == null || isNaN(t)) this._removeChildAnimations(); }, }; window.Animation = scope.Animation; if (WEB_ANIMATIONS_TESTING) { testing.webAnimationsNextAnimation = scope.Animation; } })(webAnimationsShared, webAnimationsNext, webAnimationsTesting);