• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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