1// Copyright 2014 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15(function(shared, scope, testing) { 16 scope.animationsWithPromises = []; 17 18 scope.Animation = function(effect, timeline) { 19 this.id = ''; 20 if (effect && effect._id) { 21 this.id = effect._id; 22 } 23 this.effect = effect; 24 if (effect) { 25 effect._animation = this; 26 } 27 if (!timeline) { 28 throw new Error('Animation with null timeline is not supported'); 29 } 30 this._timeline = timeline; 31 this._sequenceNumber = shared.sequenceNumber++; 32 this._holdTime = 0; 33 this._paused = false; 34 this._isGroup = false; 35 this._animation = null; 36 this._childAnimations = []; 37 this._callback = null; 38 this._oldPlayState = 'idle'; 39 this._rebuildUnderlyingAnimation(); 40 // Animations are constructed in the idle state. 41 this._animation.cancel(); 42 this._updatePromises(); 43 }; 44 45 scope.Animation.prototype = { 46 _updatePromises: function() { 47 var oldPlayState = this._oldPlayState; 48 var newPlayState = this.playState; 49 if (this._readyPromise && newPlayState !== oldPlayState) { 50 if (newPlayState == 'idle') { 51 this._rejectReadyPromise(); 52 this._readyPromise = undefined; 53 } else if (oldPlayState == 'pending') { 54 this._resolveReadyPromise(); 55 } else if (newPlayState == 'pending') { 56 this._readyPromise = undefined; 57 } 58 } 59 if (this._finishedPromise && newPlayState !== oldPlayState) { 60 if (newPlayState == 'idle') { 61 this._rejectFinishedPromise(); 62 this._finishedPromise = undefined; 63 } else if (newPlayState == 'finished') { 64 this._resolveFinishedPromise(); 65 } else if (oldPlayState == 'finished') { 66 this._finishedPromise = undefined; 67 } 68 } 69 this._oldPlayState = this.playState; 70 return (this._readyPromise || this._finishedPromise); 71 }, 72 _rebuildUnderlyingAnimation: function() { 73 this._updatePromises(); 74 var oldPlaybackRate; 75 var oldPaused; 76 var oldStartTime; 77 var oldCurrentTime; 78 var hadUnderlying = this._animation ? true : false; 79 if (hadUnderlying) { 80 oldPlaybackRate = this.playbackRate; 81 oldPaused = this._paused; 82 oldStartTime = this.startTime; 83 oldCurrentTime = this.currentTime; 84 this._animation.cancel(); 85 this._animation._wrapper = null; 86 this._animation = null; 87 } 88 89 if (!this.effect || this.effect instanceof window.KeyframeEffect) { 90 this._animation = scope.newUnderlyingAnimationForKeyframeEffect(this.effect); 91 scope.bindAnimationForKeyframeEffect(this); 92 } 93 if (this.effect instanceof window.SequenceEffect || this.effect instanceof window.GroupEffect) { 94 this._animation = scope.newUnderlyingAnimationForGroup(this.effect); 95 scope.bindAnimationForGroup(this); 96 } 97 if (this.effect && this.effect._onsample) { 98 scope.bindAnimationForCustomEffect(this); 99 } 100 if (hadUnderlying) { 101 if (oldPlaybackRate != 1) { 102 this.playbackRate = oldPlaybackRate; 103 } 104 if (oldStartTime !== null) { 105 this.startTime = oldStartTime; 106 } else if (oldCurrentTime !== null) { 107 this.currentTime = oldCurrentTime; 108 } else if (this._holdTime !== null) { 109 this.currentTime = this._holdTime; 110 } 111 if (oldPaused) { 112 this.pause(); 113 } 114 } 115 this._updatePromises(); 116 }, 117 _updateChildren: function() { 118 if (!this.effect || this.playState == 'idle') 119 return; 120 121 var offset = this.effect._timing.delay; 122 this._childAnimations.forEach(function(childAnimation) { 123 this._arrangeChildren(childAnimation, offset); 124 if (this.effect instanceof window.SequenceEffect) 125 offset += scope.groupChildDuration(childAnimation.effect); 126 }.bind(this)); 127 }, 128 _setExternalAnimation: function(animation) { 129 if (!this.effect || !this._isGroup) 130 return; 131 for (var i = 0; i < this.effect.children.length; i++) { 132 this.effect.children[i]._animation = animation; 133 this._childAnimations[i]._setExternalAnimation(animation); 134 } 135 }, 136 _constructChildAnimations: function() { 137 if (!this.effect || !this._isGroup) 138 return; 139 var offset = this.effect._timing.delay; 140 this._removeChildAnimations(); 141 this.effect.children.forEach(function(child) { 142 var childAnimation = scope.timeline._play(child); 143 this._childAnimations.push(childAnimation); 144 childAnimation.playbackRate = this.playbackRate; 145 if (this._paused) 146 childAnimation.pause(); 147 child._animation = this.effect._animation; 148 149 this._arrangeChildren(childAnimation, offset); 150 151 if (this.effect instanceof window.SequenceEffect) 152 offset += scope.groupChildDuration(child); 153 }.bind(this)); 154 }, 155 _arrangeChildren: function(childAnimation, offset) { 156 if (this.startTime === null) { 157 childAnimation.currentTime = this.currentTime - offset / this.playbackRate; 158 } else if (childAnimation.startTime !== this.startTime + offset / this.playbackRate) { 159 childAnimation.startTime = this.startTime + offset / this.playbackRate; 160 } 161 }, 162 get timeline() { 163 return this._timeline; 164 }, 165 get playState() { 166 return this._animation ? this._animation.playState : 'idle'; 167 }, 168 get finished() { 169 if (!window.Promise) { 170 console.warn('Animation Promises require JavaScript Promise constructor'); 171 return null; 172 } 173 if (!this._finishedPromise) { 174 if (scope.animationsWithPromises.indexOf(this) == -1) { 175 scope.animationsWithPromises.push(this); 176 } 177 this._finishedPromise = new Promise( 178 function(resolve, reject) { 179 this._resolveFinishedPromise = function() { 180 resolve(this); 181 }; 182 this._rejectFinishedPromise = function() { 183 reject({type: DOMException.ABORT_ERR, name: 'AbortError'}); 184 }; 185 }.bind(this)); 186 if (this.playState == 'finished') { 187 this._resolveFinishedPromise(); 188 } 189 } 190 return this._finishedPromise; 191 }, 192 get ready() { 193 if (!window.Promise) { 194 console.warn('Animation Promises require JavaScript Promise constructor'); 195 return null; 196 } 197 if (!this._readyPromise) { 198 if (scope.animationsWithPromises.indexOf(this) == -1) { 199 scope.animationsWithPromises.push(this); 200 } 201 this._readyPromise = new Promise( 202 function(resolve, reject) { 203 this._resolveReadyPromise = function() { 204 resolve(this); 205 }; 206 this._rejectReadyPromise = function() { 207 reject({type: DOMException.ABORT_ERR, name: 'AbortError'}); 208 }; 209 }.bind(this)); 210 if (this.playState !== 'pending') { 211 this._resolveReadyPromise(); 212 } 213 } 214 return this._readyPromise; 215 }, 216 get onfinish() { 217 return this._animation.onfinish; 218 }, 219 set onfinish(v) { 220 if (typeof v == 'function') { 221 this._animation.onfinish = (function(e) { 222 e.target = this; 223 v.call(this, e); 224 }).bind(this); 225 } else { 226 this._animation.onfinish = v; 227 } 228 }, 229 get oncancel() { 230 return this._animation.oncancel; 231 }, 232 set oncancel(v) { 233 if (typeof v == 'function') { 234 this._animation.oncancel = (function(e) { 235 e.target = this; 236 v.call(this, e); 237 }).bind(this); 238 } else { 239 this._animation.oncancel = v; 240 } 241 }, 242 get currentTime() { 243 this._updatePromises(); 244 var currentTime = this._animation.currentTime; 245 this._updatePromises(); 246 return currentTime; 247 }, 248 set currentTime(v) { 249 this._updatePromises(); 250 this._animation.currentTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE; 251 this._register(); 252 this._forEachChild(function(child, offset) { 253 child.currentTime = v - offset; 254 }); 255 this._updatePromises(); 256 }, 257 get startTime() { 258 return this._animation.startTime; 259 }, 260 set startTime(v) { 261 this._updatePromises(); 262 this._animation.startTime = isFinite(v) ? v : Math.sign(v) * Number.MAX_VALUE; 263 this._register(); 264 this._forEachChild(function(child, offset) { 265 child.startTime = v + offset; 266 }); 267 this._updatePromises(); 268 }, 269 get playbackRate() { 270 return this._animation.playbackRate; 271 }, 272 set playbackRate(value) { 273 this._updatePromises(); 274 var oldCurrentTime = this.currentTime; 275 this._animation.playbackRate = value; 276 this._forEachChild(function(childAnimation) { 277 childAnimation.playbackRate = value; 278 }); 279 if (oldCurrentTime !== null) { 280 this.currentTime = oldCurrentTime; 281 } 282 this._updatePromises(); 283 }, 284 play: function() { 285 this._updatePromises(); 286 this._paused = false; 287 this._animation.play(); 288 if (this._timeline._animations.indexOf(this) == -1) { 289 this._timeline._animations.push(this); 290 } 291 this._register(); 292 scope.awaitStartTime(this); 293 this._forEachChild(function(child) { 294 var time = child.currentTime; 295 child.play(); 296 child.currentTime = time; 297 }); 298 this._updatePromises(); 299 }, 300 pause: function() { 301 this._updatePromises(); 302 if (this.currentTime) { 303 this._holdTime = this.currentTime; 304 } 305 this._animation.pause(); 306 this._register(); 307 this._forEachChild(function(child) { 308 child.pause(); 309 }); 310 this._paused = true; 311 this._updatePromises(); 312 }, 313 finish: function() { 314 this._updatePromises(); 315 this._animation.finish(); 316 this._register(); 317 this._updatePromises(); 318 }, 319 cancel: function() { 320 this._updatePromises(); 321 this._animation.cancel(); 322 this._register(); 323 this._removeChildAnimations(); 324 this._updatePromises(); 325 }, 326 reverse: function() { 327 this._updatePromises(); 328 var oldCurrentTime = this.currentTime; 329 this._animation.reverse(); 330 this._forEachChild(function(childAnimation) { 331 childAnimation.reverse(); 332 }); 333 if (oldCurrentTime !== null) { 334 this.currentTime = oldCurrentTime; 335 } 336 this._updatePromises(); 337 }, 338 addEventListener: function(type, handler) { 339 var wrapped = handler; 340 if (typeof handler == 'function') { 341 wrapped = (function(e) { 342 e.target = this; 343 handler.call(this, e); 344 }).bind(this); 345 handler._wrapper = wrapped; 346 } 347 this._animation.addEventListener(type, wrapped); 348 }, 349 removeEventListener: function(type, handler) { 350 this._animation.removeEventListener(type, (handler && handler._wrapper) || handler); 351 }, 352 _removeChildAnimations: function() { 353 while (this._childAnimations.length) 354 this._childAnimations.pop().cancel(); 355 }, 356 _forEachChild: function(f) { 357 var offset = 0; 358 if (this.effect.children && this._childAnimations.length < this.effect.children.length) 359 this._constructChildAnimations(); 360 this._childAnimations.forEach(function(child) { 361 f.call(this, child, offset); 362 if (this.effect instanceof window.SequenceEffect) 363 offset += child.effect.activeDuration; 364 }.bind(this)); 365 366 if (this.playState == 'pending') 367 return; 368 var timing = this.effect._timing; 369 var t = this.currentTime; 370 if (t !== null) 371 t = shared.calculateIterationProgress(shared.calculateActiveDuration(timing), t, timing); 372 if (t == null || isNaN(t)) 373 this._removeChildAnimations(); 374 }, 375 }; 376 377 window.Animation = scope.Animation; 378 379 if (WEB_ANIMATIONS_TESTING) { 380 testing.webAnimationsNextAnimation = scope.Animation; 381 } 382 383})(webAnimationsShared, webAnimationsNext, webAnimationsTesting); 384