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