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