• 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
17  function groupChildDuration(node) {
18    return node._timing.delay + node.activeDuration + node._timing.endDelay;
19  }
20
21  function constructor(children, timingInput, id) {
22    this._id = id;
23    this._parent = null;
24    this.children = children || [];
25    this._reparent(this.children);
26    timingInput = shared.numericTimingToObject(timingInput);
27    this._timingInput = shared.cloneTimingInput(timingInput);
28    this._timing = shared.normalizeTimingInput(timingInput, true);
29    this.timing = shared.makeTiming(timingInput, true, this);
30    this.timing._effect = this;
31
32    if (this._timing.duration === 'auto') {
33      this._timing.duration = this.activeDuration;
34    }
35  }
36
37  window.SequenceEffect = function() {
38    constructor.apply(this, arguments);
39  };
40
41  window.GroupEffect = function() {
42    constructor.apply(this, arguments);
43  };
44
45  constructor.prototype = {
46    _isAncestor: function(effect) {
47      var a = this;
48      while (a !== null) {
49        if (a == effect)
50          return true;
51        a = a._parent;
52      }
53      return false;
54    },
55    _rebuild: function() {
56      // Re-calculate durations for ancestors with specified duration 'auto'.
57      var node = this;
58      while (node) {
59        if (node.timing.duration === 'auto') {
60          node._timing.duration = node.activeDuration;
61        }
62        node = node._parent;
63      }
64      if (this._animation) {
65        this._animation._rebuildUnderlyingAnimation();
66      }
67    },
68    _reparent: function(newChildren) {
69      scope.removeMulti(newChildren);
70      for (var i = 0; i < newChildren.length; i++) {
71        newChildren[i]._parent = this;
72      }
73    },
74    _putChild: function(args, isAppend) {
75      var message = isAppend ? 'Cannot append an ancestor or self' : 'Cannot prepend an ancestor or self';
76      for (var i = 0; i < args.length; i++) {
77        if (this._isAncestor(args[i])) {
78          throw {
79            type: DOMException.HIERARCHY_REQUEST_ERR,
80            name: 'HierarchyRequestError',
81            message: message
82          };
83        }
84      }
85      var oldParents = [];
86      for (var i = 0; i < args.length; i++) {
87        isAppend ? this.children.push(args[i]) : this.children.unshift(args[i]);
88      }
89      this._reparent(args);
90      this._rebuild();
91    },
92    append: function()  {
93      this._putChild(arguments, true);
94    },
95    prepend: function()  {
96      this._putChild(arguments, false);
97    },
98    get parent() {
99      return this._parent;
100    },
101    get firstChild() {
102      return this.children.length ? this.children[0] : null;
103    },
104    get lastChild() {
105      return this.children.length ? this.children[this.children.length - 1] : null;
106    },
107    clone: function() {
108      var clonedTiming = shared.cloneTimingInput(this._timingInput);
109      var clonedChildren = [];
110      for (var i = 0; i < this.children.length; i++) {
111        clonedChildren.push(this.children[i].clone());
112      }
113      return (this instanceof GroupEffect) ?
114          new GroupEffect(clonedChildren, clonedTiming) :
115          new SequenceEffect(clonedChildren, clonedTiming);
116    },
117    remove: function() {
118      scope.removeMulti([this]);
119    }
120  };
121
122  window.SequenceEffect.prototype = Object.create(constructor.prototype);
123  Object.defineProperty(
124      window.SequenceEffect.prototype,
125      'activeDuration',
126      {
127        get: function() {
128          var total = 0;
129          this.children.forEach(function(child) {
130            total += groupChildDuration(child);
131          });
132          return Math.max(total, 0);
133        }
134      });
135
136  window.GroupEffect.prototype = Object.create(constructor.prototype);
137  Object.defineProperty(
138      window.GroupEffect.prototype,
139      'activeDuration',
140      {
141        get: function() {
142          var max = 0;
143          this.children.forEach(function(child) {
144            max = Math.max(max, groupChildDuration(child));
145          });
146          return max;
147        }
148      });
149
150  scope.newUnderlyingAnimationForGroup = function(group) {
151    var underlyingAnimation;
152    var timing = null;
153    var ticker = function(tf) {
154      var animation = underlyingAnimation._wrapper;
155      if (!animation) {
156        return;
157      }
158      if (animation.playState == 'pending') {
159        return;
160      }
161      if (!animation.effect) {
162        return;
163      }
164      if (tf == null) {
165        animation._removeChildAnimations();
166        return;
167      }
168
169      // If the group has a negative playback rate and is not fill backwards/both, then it should go
170      // out of effect when it reaches the start of its active interval (tf == 0). If it is fill
171      // backwards/both then it should stay in effect. calculateIterationProgress will return 0 in the
172      // backwards-filling case, and null otherwise.
173      if (tf == 0 && animation.playbackRate < 0) {
174        if (!timing) {
175          timing = shared.normalizeTimingInput(animation.effect.timing);
176        }
177        tf = shared.calculateIterationProgress(shared.calculateActiveDuration(timing), -1, timing);
178        if (isNaN(tf) || tf == null) {
179          animation._forEachChild(function(child) {
180            child.currentTime = -1;
181          });
182          animation._removeChildAnimations();
183          return;
184        }
185      }
186    };
187
188    var underlyingEffect = new KeyframeEffect(null, [], group._timing, group._id);
189    underlyingEffect.onsample = ticker;
190    underlyingAnimation = scope.timeline._play(underlyingEffect);
191    return underlyingAnimation;
192  };
193
194  scope.bindAnimationForGroup = function(animation) {
195    animation._animation._wrapper = animation;
196    animation._isGroup = true;
197    scope.awaitStartTime(animation);
198    animation._constructChildAnimations();
199    animation._setExternalAnimation(animation);
200  };
201
202  scope.groupChildDuration = groupChildDuration;
203
204})(webAnimationsShared, webAnimationsNext, webAnimationsTesting);
205