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 17 shared.sequenceNumber = 0; 18 19 var AnimationEvent = function(target, currentTime, timelineTime) { 20 this.target = target; 21 this.currentTime = currentTime; 22 this.timelineTime = timelineTime; 23 24 this.type = 'finish'; 25 this.bubbles = false; 26 this.cancelable = false; 27 this.currentTarget = target; 28 this.defaultPrevented = false; 29 this.eventPhase = Event.AT_TARGET; 30 this.timeStamp = Date.now(); 31 }; 32 33 scope.Animation = function(effect) { 34 this.id = ''; 35 if (effect && effect._id) { 36 this.id = effect._id; 37 } 38 this._sequenceNumber = shared.sequenceNumber++; 39 this._currentTime = 0; 40 this._startTime = null; 41 this._paused = false; 42 this._playbackRate = 1; 43 this._inTimeline = true; 44 this._finishedFlag = true; 45 this.onfinish = null; 46 this._finishHandlers = []; 47 this._effect = effect; 48 this._inEffect = this._effect._update(0); 49 this._idle = true; 50 this._currentTimePending = false; 51 }; 52 53 scope.Animation.prototype = { 54 _ensureAlive: function() { 55 // If an animation is playing backwards and is not fill backwards/both 56 // then it should go out of effect when it reaches the start of its 57 // active interval (currentTime == 0). 58 if (this.playbackRate < 0 && this.currentTime === 0) { 59 this._inEffect = this._effect._update(-1); 60 } else { 61 this._inEffect = this._effect._update(this.currentTime); 62 } 63 if (!this._inTimeline && (this._inEffect || !this._finishedFlag)) { 64 this._inTimeline = true; 65 scope.timeline._animations.push(this); 66 } 67 }, 68 _tickCurrentTime: function(newTime, ignoreLimit) { 69 if (newTime != this._currentTime) { 70 this._currentTime = newTime; 71 if (this._isFinished && !ignoreLimit) 72 this._currentTime = this._playbackRate > 0 ? this._totalDuration : 0; 73 this._ensureAlive(); 74 } 75 }, 76 get currentTime() { 77 if (this._idle || this._currentTimePending) 78 return null; 79 return this._currentTime; 80 }, 81 set currentTime(newTime) { 82 newTime = +newTime; 83 if (isNaN(newTime)) 84 return; 85 scope.restart(); 86 if (!this._paused && this._startTime != null) { 87 this._startTime = this._timeline.currentTime - newTime / this._playbackRate; 88 } 89 this._currentTimePending = false; 90 if (this._currentTime == newTime) 91 return; 92 if (this._idle) { 93 this._idle = false; 94 this._paused = true; 95 } 96 this._tickCurrentTime(newTime, true); 97 scope.applyDirtiedAnimation(this); 98 }, 99 get startTime() { 100 return this._startTime; 101 }, 102 set startTime(newTime) { 103 newTime = +newTime; 104 if (isNaN(newTime)) 105 return; 106 if (this._paused || this._idle) 107 return; 108 this._startTime = newTime; 109 this._tickCurrentTime((this._timeline.currentTime - this._startTime) * this.playbackRate); 110 scope.applyDirtiedAnimation(this); 111 }, 112 get playbackRate() { 113 return this._playbackRate; 114 }, 115 set playbackRate(value) { 116 if (value == this._playbackRate) { 117 return; 118 } 119 var oldCurrentTime = this.currentTime; 120 this._playbackRate = value; 121 this._startTime = null; 122 if (this.playState != 'paused' && this.playState != 'idle') { 123 this._finishedFlag = false; 124 this._idle = false; 125 this._ensureAlive(); 126 scope.applyDirtiedAnimation(this); 127 } 128 if (oldCurrentTime != null) { 129 this.currentTime = oldCurrentTime; 130 } 131 }, 132 get _isFinished() { 133 return !this._idle && (this._playbackRate > 0 && this._currentTime >= this._totalDuration || 134 this._playbackRate < 0 && this._currentTime <= 0); 135 }, 136 get _totalDuration() { return this._effect._totalDuration; }, 137 get playState() { 138 if (this._idle) 139 return 'idle'; 140 if ((this._startTime == null && !this._paused && this.playbackRate != 0) || this._currentTimePending) 141 return 'pending'; 142 if (this._paused) 143 return 'paused'; 144 if (this._isFinished) 145 return 'finished'; 146 return 'running'; 147 }, 148 _rewind: function() { 149 if (this._playbackRate >= 0) { 150 this._currentTime = 0; 151 } else if (this._totalDuration < Infinity) { 152 this._currentTime = this._totalDuration; 153 } else { 154 throw new DOMException( 155 'Unable to rewind negative playback rate animation with infinite duration', 156 'InvalidStateError'); 157 } 158 }, 159 play: function() { 160 this._paused = false; 161 if (this._isFinished || this._idle) { 162 this._rewind(); 163 this._startTime = null; 164 } 165 this._finishedFlag = false; 166 this._idle = false; 167 this._ensureAlive(); 168 scope.applyDirtiedAnimation(this); 169 }, 170 pause: function() { 171 if (!this._isFinished && !this._paused && !this._idle) { 172 this._currentTimePending = true; 173 } else if (this._idle) { 174 this._rewind(); 175 this._idle = false; 176 } 177 this._startTime = null; 178 this._paused = true; 179 }, 180 finish: function() { 181 if (this._idle) 182 return; 183 this.currentTime = this._playbackRate > 0 ? this._totalDuration : 0; 184 this._startTime = this._totalDuration - this.currentTime; 185 this._currentTimePending = false; 186 scope.applyDirtiedAnimation(this); 187 }, 188 cancel: function() { 189 if (!this._inEffect) 190 return; 191 this._inEffect = false; 192 this._idle = true; 193 this._paused = false; 194 this._isFinished = true; 195 this._finishedFlag = true; 196 this._currentTime = 0; 197 this._startTime = null; 198 this._effect._update(null); 199 // effects are invalid after cancellation as the animation state 200 // needs to un-apply. 201 scope.applyDirtiedAnimation(this); 202 }, 203 reverse: function() { 204 this.playbackRate *= -1; 205 this.play(); 206 }, 207 addEventListener: function(type, handler) { 208 if (typeof handler == 'function' && type == 'finish') 209 this._finishHandlers.push(handler); 210 }, 211 removeEventListener: function(type, handler) { 212 if (type != 'finish') 213 return; 214 var index = this._finishHandlers.indexOf(handler); 215 if (index >= 0) 216 this._finishHandlers.splice(index, 1); 217 }, 218 _fireEvents: function(baseTime) { 219 if (this._isFinished) { 220 if (!this._finishedFlag) { 221 var event = new AnimationEvent(this, this._currentTime, baseTime); 222 var handlers = this._finishHandlers.concat(this.onfinish ? [this.onfinish] : []); 223 setTimeout(function() { 224 handlers.forEach(function(handler) { 225 handler.call(event.target, event); 226 }); 227 }, 0); 228 this._finishedFlag = true; 229 } 230 } else { 231 this._finishedFlag = false; 232 } 233 }, 234 _tick: function(timelineTime, isAnimationFrame) { 235 if (!this._idle && !this._paused) { 236 if (this._startTime == null) { 237 if (isAnimationFrame) { 238 this.startTime = timelineTime - this._currentTime / this.playbackRate; 239 } 240 } else if (!this._isFinished) { 241 this._tickCurrentTime((timelineTime - this._startTime) * this.playbackRate); 242 } 243 } 244 245 if (isAnimationFrame) { 246 this._currentTimePending = false; 247 this._fireEvents(timelineTime); 248 } 249 }, 250 get _needsTick() { 251 return (this.playState in {'pending': 1, 'running': 1}) || !this._finishedFlag; 252 }, 253 _targetAnimations: function() { 254 var target = this._effect._target; 255 if (!target._activeAnimations) { 256 target._activeAnimations = []; 257 } 258 return target._activeAnimations; 259 }, 260 _markTarget: function() { 261 var animations = this._targetAnimations(); 262 if (animations.indexOf(this) === -1) { 263 animations.push(this); 264 } 265 }, 266 _unmarkTarget: function() { 267 var animations = this._targetAnimations(); 268 var index = animations.indexOf(this); 269 if (index !== -1) { 270 animations.splice(index, 1); 271 } 272 }, 273 }; 274 275 if (WEB_ANIMATIONS_TESTING) { 276 testing.webAnimations1Animation = scope.Animation; 277 } 278 279})(webAnimationsShared, webAnimations1, webAnimationsTesting); 280