UNPKG

14.3 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, testing) {
16
17 var fills = 'backwards|forwards|both|none'.split('|');
18 var directions = 'reverse|alternate|alternate-reverse'.split('|');
19 var linear = function(x) { return x; };
20
21 function cloneTimingInput(timingInput) {
22 if (typeof timingInput == 'number') {
23 return timingInput;
24 }
25 var clone = {};
26 for (var m in timingInput) {
27 clone[m] = timingInput[m];
28 }
29 return clone;
30 }
31
32 function AnimationEffectTiming() {
33 this._delay = 0;
34 this._endDelay = 0;
35 this._fill = 'none';
36 this._iterationStart = 0;
37 this._iterations = 1;
38 this._duration = 0;
39 this._playbackRate = 1;
40 this._direction = 'normal';
41 this._easing = 'linear';
42 this._easingFunction = linear;
43 }
44
45 function isInvalidTimingDeprecated() {
46 return shared.isDeprecated('Invalid timing inputs', '2016-03-02', 'TypeError exceptions will be thrown instead.', true);
47 }
48
49 AnimationEffectTiming.prototype = {
50 _setMember: function(member, value) {
51 this['_' + member] = value;
52 if (this._effect) {
53 this._effect._timingInput[member] = value;
54 this._effect._timing = shared.normalizeTimingInput(this._effect._timingInput);
55 this._effect.activeDuration = shared.calculateActiveDuration(this._effect._timing);
56 if (this._effect._animation) {
57 this._effect._animation._rebuildUnderlyingAnimation();
58 }
59 }
60 },
61 get playbackRate() {
62 return this._playbackRate;
63 },
64 set delay(value) {
65 this._setMember('delay', value);
66 },
67 get delay() {
68 return this._delay;
69 },
70 set endDelay(value) {
71 this._setMember('endDelay', value);
72 },
73 get endDelay() {
74 return this._endDelay;
75 },
76 set fill(value) {
77 this._setMember('fill', value);
78 },
79 get fill() {
80 return this._fill;
81 },
82 set iterationStart(value) {
83 if ((isNaN(value) || value < 0) && isInvalidTimingDeprecated()) {
84 throw new TypeError('iterationStart must be a non-negative number, received: ' + value);
85 }
86 this._setMember('iterationStart', value);
87 },
88 get iterationStart() {
89 return this._iterationStart;
90 },
91 set duration(value) {
92 if (value != 'auto' && (isNaN(value) || value < 0) && isInvalidTimingDeprecated()) {
93 throw new TypeError('duration must be non-negative or auto, received: ' + value);
94 }
95 this._setMember('duration', value);
96 },
97 get duration() {
98 return this._duration;
99 },
100 set direction(value) {
101 this._setMember('direction', value);
102 },
103 get direction() {
104 return this._direction;
105 },
106 set easing(value) {
107 this._easingFunction = parseEasingFunction(normalizeEasing(value));
108 this._setMember('easing', value);
109 },
110 get easing() {
111 return this._easing;
112 },
113 set iterations(value) {
114 if ((isNaN(value) || value < 0) && isInvalidTimingDeprecated()) {
115 throw new TypeError('iterations must be non-negative, received: ' + value);
116 }
117 this._setMember('iterations', value);
118 },
119 get iterations() {
120 return this._iterations;
121 }
122 };
123
124 function makeTiming(timingInput, forGroup, effect) {
125 var timing = new AnimationEffectTiming();
126 if (forGroup) {
127 timing.fill = 'both';
128 timing.duration = 'auto';
129 }
130 if (typeof timingInput == 'number' && !isNaN(timingInput)) {
131 timing.duration = timingInput;
132 } else if (timingInput !== undefined) {
133 Object.getOwnPropertyNames(timingInput).forEach(function(property) {
134 if (timingInput[property] != 'auto') {
135 if (typeof timing[property] == 'number' || property == 'duration') {
136 if (typeof timingInput[property] != 'number' || isNaN(timingInput[property])) {
137 return;
138 }
139 }
140 if ((property == 'fill') && (fills.indexOf(timingInput[property]) == -1)) {
141 return;
142 }
143 if ((property == 'direction') && (directions.indexOf(timingInput[property]) == -1)) {
144 return;
145 }
146 if (property == 'playbackRate' && timingInput[property] !== 1 && shared.isDeprecated('AnimationEffectTiming.playbackRate', '2014-11-28', 'Use Animation.playbackRate instead.')) {
147 return;
148 }
149 timing[property] = timingInput[property];
150 }
151 });
152 }
153 return timing;
154 }
155
156 function numericTimingToObject(timingInput) {
157 if (typeof timingInput == 'number') {
158 if (isNaN(timingInput)) {
159 timingInput = { duration: 0 };
160 } else {
161 timingInput = { duration: timingInput };
162 }
163 }
164 return timingInput;
165 }
166
167 function normalizeTimingInput(timingInput, forGroup) {
168 timingInput = shared.numericTimingToObject(timingInput);
169 return makeTiming(timingInput, forGroup);
170 }
171
172 function cubic(a, b, c, d) {
173 if (a < 0 || a > 1 || c < 0 || c > 1) {
174 return linear;
175 }
176 return function(x) {
177 if (x <= 0) {
178 var start_gradient = 0;
179 if (a > 0)
180 start_gradient = b / a;
181 else if (!b && c > 0)
182 start_gradient = d / c;
183 return start_gradient * x;
184 }
185 if (x >= 1) {
186 var end_gradient = 0;
187 if (c < 1)
188 end_gradient = (d - 1) / (c - 1);
189 else if (c == 1 && a < 1)
190 end_gradient = (b - 1) / (a - 1);
191 return 1 + end_gradient * (x - 1);
192 }
193
194 var start = 0, end = 1;
195 function f(a, b, m) { return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m};
196 while (start < end) {
197 var mid = (start + end) / 2;
198 var xEst = f(a, c, mid);
199 if (Math.abs(x - xEst) < 0.00001) {
200 return f(b, d, mid);
201 }
202 if (xEst < x) {
203 start = mid;
204 } else {
205 end = mid;
206 }
207 }
208 return f(b, d, mid);
209 }
210 }
211
212 var Start = 1;
213 var Middle = 0.5;
214 var End = 0;
215
216 function step(count, pos) {
217 return function(x) {
218 if (x >= 1) {
219 return 1;
220 }
221 var stepSize = 1 / count;
222 x += pos * stepSize;
223 return x - x % stepSize;
224 }
225 }
226
227 var presets = {
228 'ease': cubic(0.25, 0.1, 0.25, 1),
229 'ease-in': cubic(0.42, 0, 1, 1),
230 'ease-out': cubic(0, 0, 0.58, 1),
231 'ease-in-out': cubic(0.42, 0, 0.58, 1),
232 'step-start': step(1, Start),
233 'step-middle': step(1, Middle),
234 'step-end': step(1, End)
235 };
236
237 var styleForCleaning = null;
238 var numberString = '\\s*(-?\\d+\\.?\\d*|-?\\.\\d+)\\s*';
239 var cubicBezierRe = new RegExp('cubic-bezier\\(' + numberString + ',' + numberString + ',' + numberString + ',' + numberString + '\\)');
240 var step1Re = /steps\(\s*(\d+)\s*\)/;
241 var step2Re = /steps\(\s*(\d+)\s*,\s*(start|middle|end)\s*\)/;
242
243 function normalizeEasing(easing) {
244 if (!styleForCleaning) {
245 styleForCleaning = document.createElement('div').style;
246 }
247 styleForCleaning.animationTimingFunction = '';
248 styleForCleaning.animationTimingFunction = easing;
249 var normalizedEasing = styleForCleaning.animationTimingFunction;
250 if (normalizedEasing == '' && isInvalidTimingDeprecated()) {
251 throw new TypeError(easing + ' is not a valid value for easing');
252 }
253 return normalizedEasing;
254 }
255
256 function parseEasingFunction(normalizedEasing) {
257 if (normalizedEasing == 'linear') {
258 return linear;
259 }
260 var cubicData = cubicBezierRe.exec(normalizedEasing);
261 if (cubicData) {
262 return cubic.apply(this, cubicData.slice(1).map(Number));
263 }
264 var step1Data = step1Re.exec(normalizedEasing);
265 if (step1Data) {
266 return step(Number(step1Data[1]), End);
267 }
268 var step2Data = step2Re.exec(normalizedEasing);
269 if (step2Data) {
270 return step(Number(step2Data[1]), {'start': Start, 'middle': Middle, 'end': End}[step2Data[2]]);
271 }
272 var preset = presets[normalizedEasing];
273 if (preset) {
274 return preset;
275 }
276 // At this point none of our parse attempts succeeded; the easing is invalid.
277 // Fall back to linear in the interest of not crashing the page.
278 return linear;
279 }
280
281 function calculateActiveDuration(timing) {
282 return Math.abs(repeatedDuration(timing) / timing.playbackRate);
283 }
284
285 function repeatedDuration(timing) {
286 // https://drafts.csswg.org/web-animations/#calculating-the-active-duration
287 if (timing.duration === 0 || timing.iterations === 0) {
288 return 0;
289 }
290 return timing.duration * timing.iterations;
291 }
292
293 var PhaseNone = 0;
294 var PhaseBefore = 1;
295 var PhaseAfter = 2;
296 var PhaseActive = 3;
297
298 function calculatePhase(activeDuration, localTime, timing) {
299 // https://drafts.csswg.org/web-animations/#animation-effect-phases-and-states
300 if (localTime == null) {
301 return PhaseNone;
302 }
303
304 var endTime = timing.delay + activeDuration + timing.endDelay;
305 if (localTime < Math.min(timing.delay, endTime)) {
306 return PhaseBefore;
307 }
308 if (localTime >= Math.min(timing.delay + activeDuration, endTime)) {
309 return PhaseAfter;
310 }
311
312 return PhaseActive;
313 }
314
315 function calculateActiveTime(activeDuration, fillMode, localTime, phase, delay) {
316 // https://drafts.csswg.org/web-animations/#calculating-the-active-time
317 switch (phase) {
318 case PhaseBefore:
319 if (fillMode == 'backwards' || fillMode == 'both')
320 return 0;
321 return null;
322 case PhaseActive:
323 return localTime - delay;
324 case PhaseAfter:
325 if (fillMode == 'forwards' || fillMode == 'both')
326 return activeDuration;
327 return null;
328 case PhaseNone:
329 return null;
330 }
331 }
332
333 function calculateOverallProgress(iterationDuration, phase, iterations, activeTime, iterationStart) {
334 // https://drafts.csswg.org/web-animations/#calculating-the-overall-progress
335 var overallProgress = iterationStart;
336 if (iterationDuration === 0) {
337 if (phase !== PhaseBefore) {
338 overallProgress += iterations;
339 }
340 } else {
341 overallProgress += activeTime / iterationDuration;
342 }
343 return overallProgress;
344 }
345
346 function calculateSimpleIterationProgress(overallProgress, iterationStart, phase, iterations, activeTime, iterationDuration) {
347 // https://drafts.csswg.org/web-animations/#calculating-the-simple-iteration-progress
348
349 var simpleIterationProgress = (overallProgress === Infinity) ? iterationStart % 1 : overallProgress % 1;
350 if (simpleIterationProgress === 0 && phase === PhaseAfter && iterations !== 0 &&
351 (activeTime !== 0 || iterationDuration === 0)) {
352 simpleIterationProgress = 1;
353 }
354 return simpleIterationProgress;
355 }
356
357 function calculateCurrentIteration(phase, iterations, simpleIterationProgress, overallProgress) {
358 // https://drafts.csswg.org/web-animations/#calculating-the-current-iteration
359 if (phase === PhaseAfter && iterations === Infinity) {
360 return Infinity;
361 }
362 if (simpleIterationProgress === 1) {
363 return Math.floor(overallProgress) - 1;
364 }
365 return Math.floor(overallProgress);
366 }
367
368 function calculateDirectedProgress(playbackDirection, currentIteration, simpleIterationProgress) {
369 // https://drafts.csswg.org/web-animations/#calculating-the-directed-progress
370 var currentDirection = playbackDirection;
371 if (playbackDirection !== 'normal' && playbackDirection !== 'reverse') {
372 var d = currentIteration;
373 if (playbackDirection === 'alternate-reverse') {
374 d += 1;
375 }
376 currentDirection = 'normal';
377 if (d !== Infinity && d % 2 !== 0) {
378 currentDirection = 'reverse';
379 }
380 }
381 if (currentDirection === 'normal') {
382 return simpleIterationProgress;
383 }
384 return 1 - simpleIterationProgress;
385 }
386
387 function calculateIterationProgress(activeDuration, localTime, timing) {
388 var phase = calculatePhase(activeDuration, localTime, timing);
389 var activeTime = calculateActiveTime(activeDuration, timing.fill, localTime, phase, timing.delay);
390 if (activeTime === null)
391 return null;
392
393 var overallProgress = calculateOverallProgress(timing.duration, phase, timing.iterations, activeTime, timing.iterationStart);
394 var simpleIterationProgress = calculateSimpleIterationProgress(overallProgress, timing.iterationStart, phase, timing.iterations, activeTime, timing.duration);
395 var currentIteration = calculateCurrentIteration(phase, timing.iterations, simpleIterationProgress, overallProgress);
396 var directedProgress = calculateDirectedProgress(timing.direction, currentIteration, simpleIterationProgress);
397
398 // https://drafts.csswg.org/web-animations/#calculating-the-transformed-progress
399 // https://drafts.csswg.org/web-animations/#calculating-the-iteration-progress
400 return timing._easingFunction(directedProgress);
401 }
402
403 shared.cloneTimingInput = cloneTimingInput;
404 shared.makeTiming = makeTiming;
405 shared.numericTimingToObject = numericTimingToObject;
406 shared.normalizeTimingInput = normalizeTimingInput;
407 shared.calculateActiveDuration = calculateActiveDuration;
408 shared.calculateIterationProgress = calculateIterationProgress;
409 shared.calculatePhase = calculatePhase;
410 shared.normalizeEasing = normalizeEasing;
411 shared.parseEasingFunction = parseEasingFunction;
412
413 if (WEB_ANIMATIONS_TESTING) {
414 testing.normalizeTimingInput = normalizeTimingInput;
415 testing.normalizeEasing = normalizeEasing;
416 testing.parseEasingFunction = parseEasingFunction;
417 testing.calculateActiveDuration = calculateActiveDuration;
418 testing.calculatePhase = calculatePhase;
419 testing.PhaseNone = PhaseNone;
420 testing.PhaseBefore = PhaseBefore;
421 testing.PhaseActive = PhaseActive;
422 testing.PhaseAfter = PhaseAfter;
423 }
424
425})(webAnimationsShared, webAnimationsTesting);