• 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, 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: ' + timing.iterationStart);
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      while (start < end) {
196        var mid = (start + end) / 2;
197        function f(a, b, m) { return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m};
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 stepRe = /steps\(\s*(\d+)\s*,\s*(start|middle|end)\s*\)/;
241
242  function normalizeEasing(easing) {
243    if (!styleForCleaning) {
244      styleForCleaning = document.createElement('div').style;
245    }
246    styleForCleaning.animationTimingFunction = '';
247    styleForCleaning.animationTimingFunction = easing;
248    var normalizedEasing = styleForCleaning.animationTimingFunction;
249    if (normalizedEasing == '' && isInvalidTimingDeprecated()) {
250      throw new TypeError(easing + ' is not a valid value for easing');
251    }
252    return normalizedEasing;
253  }
254
255  function parseEasingFunction(normalizedEasing) {
256    if (normalizedEasing == 'linear') {
257      return linear;
258    }
259    var cubicData = cubicBezierRe.exec(normalizedEasing);
260    if (cubicData) {
261      return cubic.apply(this, cubicData.slice(1).map(Number));
262    }
263    var stepData = stepRe.exec(normalizedEasing);
264    if (stepData) {
265      return step(Number(stepData[1]), {'start': Start, 'middle': Middle, 'end': End}[stepData[2]]);
266    }
267    var preset = presets[normalizedEasing];
268    if (preset) {
269      return preset;
270    }
271    // At this point none of our parse attempts succeeded; the easing is invalid.
272    // Fall back to linear in the interest of not crashing the page.
273    return linear;
274  }
275
276  function calculateActiveDuration(timing) {
277    return Math.abs(repeatedDuration(timing) / timing.playbackRate);
278  }
279
280  function repeatedDuration(timing) {
281    // https://w3c.github.io/web-animations/#calculating-the-active-duration
282    if (timing.duration === 0 || timing.iterations === 0) {
283      return 0;
284    }
285    return timing.duration * timing.iterations;
286  }
287
288  var PhaseNone = 0;
289  var PhaseBefore = 1;
290  var PhaseAfter = 2;
291  var PhaseActive = 3;
292
293  function calculatePhase(activeDuration, localTime, timing) {
294    // https://w3c.github.io/web-animations/#animation-effect-phases-and-states
295    if (localTime == null) {
296      return PhaseNone;
297    }
298
299    var endTime = timing.delay + activeDuration + timing.endDelay;
300    if (localTime < Math.min(timing.delay, endTime)) {
301      return PhaseBefore;
302    }
303    if (localTime >= Math.min(timing.delay + activeDuration, endTime)) {
304      return PhaseAfter;
305    }
306
307    return PhaseActive;
308  }
309
310  function calculateActiveTime(activeDuration, fillMode, localTime, phase, delay) {
311    // https://w3c.github.io/web-animations/#calculating-the-active-time
312    switch (phase) {
313      case PhaseBefore:
314        if (fillMode == 'backwards' || fillMode == 'both')
315          return 0;
316        return null;
317      case PhaseActive:
318        return localTime - delay;
319      case PhaseAfter:
320        if (fillMode == 'forwards' || fillMode == 'both')
321          return activeDuration;
322        return null;
323      case PhaseNone:
324        return null;
325    }
326  }
327
328  function calculateOverallProgress(iterationDuration, phase, iterations, activeTime, iterationStart) {
329    // https://w3c.github.io/web-animations/#calculating-the-overall-progress
330    var overallProgress = iterationStart;
331    if (iterationDuration === 0) {
332      if (phase !== PhaseBefore) {
333        overallProgress += iterations;
334      }
335    } else {
336      overallProgress += activeTime / iterationDuration;
337    }
338    return overallProgress;
339  }
340
341  function calculateSimpleIterationProgress(overallProgress, iterationStart, phase, iterations, activeTime, iterationDuration) {
342    // https://w3c.github.io/web-animations/#calculating-the-simple-iteration-progress
343
344    var simpleIterationProgress = (overallProgress === Infinity) ? iterationStart % 1 : overallProgress % 1;
345    if (simpleIterationProgress === 0 && phase === PhaseAfter && iterations !== 0 &&
346        (activeTime !== 0 || iterationDuration === 0)) {
347      simpleIterationProgress = 1;
348    }
349    return simpleIterationProgress;
350  }
351
352  function calculateCurrentIteration(phase, iterations, simpleIterationProgress, overallProgress) {
353    // https://w3c.github.io/web-animations/#calculating-the-current-iteration
354    if (phase === PhaseAfter && iterations === Infinity) {
355      return Infinity;
356    }
357    if (simpleIterationProgress === 1) {
358      return Math.floor(overallProgress) - 1;
359    }
360    return Math.floor(overallProgress);
361  }
362
363  function calculateDirectedProgress(playbackDirection, currentIteration, simpleIterationProgress) {
364    // https://w3c.github.io/web-animations/#calculating-the-directed-progress
365    var currentDirection = playbackDirection;
366    if (playbackDirection !== 'normal' && playbackDirection !== 'reverse') {
367      var d = currentIteration;
368      if (playbackDirection === 'alternate-reverse') {
369        d += 1;
370      }
371      currentDirection = 'normal';
372      if (d !== Infinity && d % 2 !== 0) {
373        currentDirection = 'reverse';
374      }
375    }
376    if (currentDirection === 'normal') {
377      return simpleIterationProgress;
378    }
379    return 1 - simpleIterationProgress;
380  }
381
382  function calculateIterationProgress(activeDuration, localTime, timing) {
383    var phase = calculatePhase(activeDuration, localTime, timing);
384    var activeTime = calculateActiveTime(activeDuration, timing.fill, localTime, phase, timing.delay);
385    if (activeTime === null)
386      return null;
387
388    var overallProgress = calculateOverallProgress(timing.duration, phase, timing.iterations, activeTime, timing.iterationStart);
389    var simpleIterationProgress = calculateSimpleIterationProgress(overallProgress, timing.iterationStart, phase, timing.iterations, activeTime, timing.duration);
390    var currentIteration = calculateCurrentIteration(phase, timing.iterations, simpleIterationProgress, overallProgress);
391    var directedProgress = calculateDirectedProgress(timing.direction, currentIteration, simpleIterationProgress);
392
393    // https://w3c.github.io/web-animations/#calculating-the-transformed-progress
394    // https://w3c.github.io/web-animations/#calculating-the-iteration-progress
395    return timing._easingFunction(directedProgress);
396  }
397
398  shared.cloneTimingInput = cloneTimingInput;
399  shared.makeTiming = makeTiming;
400  shared.numericTimingToObject = numericTimingToObject;
401  shared.normalizeTimingInput = normalizeTimingInput;
402  shared.calculateActiveDuration = calculateActiveDuration;
403  shared.calculateIterationProgress = calculateIterationProgress;
404  shared.calculatePhase = calculatePhase;
405  shared.normalizeEasing = normalizeEasing;
406  shared.parseEasingFunction = parseEasingFunction;
407
408  if (WEB_ANIMATIONS_TESTING) {
409    testing.normalizeTimingInput = normalizeTimingInput;
410    testing.normalizeEasing = normalizeEasing;
411    testing.parseEasingFunction = parseEasingFunction;
412    testing.calculateActiveDuration = calculateActiveDuration;
413    testing.calculatePhase = calculatePhase;
414    testing.PhaseNone = PhaseNone;
415    testing.PhaseBefore = PhaseBefore;
416    testing.PhaseActive = PhaseActive;
417    testing.PhaseAfter = PhaseAfter;
418  }
419
420})(webAnimationsShared, webAnimationsTesting);
421