UNPKG

12.7 kBJavaScriptView Raw
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);