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
20  function makeTiming(timingInput, forGroup) {
21    var timing = {
22      delay: 0,
23      endDelay: 0,
24      fill: forGroup ? 'both' : 'none',
25      iterationStart: 0,
26      iterations: 1,
27      duration: forGroup ? 'auto' : 0,
28      playbackRate: 1,
29      direction: 'normal',
30      easing: 'linear',
31    };
32    if (typeof timingInput == 'number' && !isNaN(timingInput)) {
33      timing.duration = timingInput;
34    } else if (timingInput !== undefined) {
35      Object.getOwnPropertyNames(timingInput).forEach(function(property) {
36        if (timingInput[property] != 'auto') {
37          if (typeof timing[property] == 'number' || property == 'duration') {
38            if (typeof timingInput[property] != 'number' || isNaN(timingInput[property])) {
39              return;
40            }
41          }
42          if ((property == 'fill') && (fills.indexOf(timingInput[property]) == -1)) {
43            return;
44          }
45          if ((property == 'direction') && (directions.indexOf(timingInput[property]) == -1)) {
46            return;
47          }
48          if (property == 'playbackRate' && timingInput[property] !== 1 && shared.isDeprecated('AnimationTiming.playbackRate', '2014-11-28', 'Use AnimationPlayer.playbackRate instead.')) {
49            return;
50          }
51          timing[property] = timingInput[property];
52        }
53      });
54    }
55    return timing;
56  }
57
58  function normalizeTimingInput(timingInput, forGroup) {
59    var timing = makeTiming(timingInput, forGroup);
60    timing.easing = toTimingFunction(timing.easing);
61    return timing;
62  }
63
64  function cubic(a, b, c, d) {
65    if (a < 0 || a > 1 || c < 0 || c > 1) {
66      return linear;
67    }
68    return function(x) {
69      var start = 0, end = 1;
70      while (1) {
71        var mid = (start + end) / 2;
72        function f(a, b, m) { return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m};
73        var xEst = f(a, c, mid);
74        if (Math.abs(x - xEst) < 0.001) {
75          return f(b, d, mid);
76        }
77        if (xEst < x) {
78          start = mid;
79        } else {
80          end = mid;
81        }
82      }
83    }
84  }
85
86  var Start = 1;
87  var Middle = 0.5;
88  var End = 0;
89
90  function step(count, pos) {
91    return function(x) {
92      if (x >= 1) {
93        return 1;
94      }
95      var stepSize = 1 / count;
96      x += pos * stepSize;
97      return x - x % stepSize;
98    }
99  }
100
101  var presets = {
102    'ease': cubic(0.25, 0.1, 0.25, 1),
103    'ease-in': cubic(0.42, 0, 1, 1),
104    'ease-out': cubic(0, 0, 0.58, 1),
105    'ease-in-out': cubic(0.42, 0, 0.58, 1),
106    'step-start': step(1, Start),
107    'step-middle': step(1, Middle),
108    'step-end': step(1, End)
109  };
110
111  var numberString = '\\s*(-?\\d+\\.?\\d*|-?\\.\\d+)\\s*';
112  var cubicBezierRe = new RegExp('cubic-bezier\\(' + numberString + ',' + numberString + ',' + numberString + ',' + numberString + '\\)');
113  var stepRe = /steps\(\s*(\d+)\s*,\s*(start|middle|end)\s*\)/;
114  var linear = function(x) { return x; };
115
116  function toTimingFunction(easing) {
117    var cubicData = cubicBezierRe.exec(easing);
118    if (cubicData) {
119      return cubic.apply(this, cubicData.slice(1).map(Number));
120    }
121    var stepData = stepRe.exec(easing);
122    if (stepData) {
123      return step(Number(stepData[1]), {'start': Start, 'middle': Middle, 'end': End}[stepData[2]]);
124    }
125    var preset = presets[easing];
126    if (preset) {
127      return preset;
128    }
129    return linear;
130  };
131
132  function calculateActiveDuration(timing) {
133    return Math.abs(repeatedDuration(timing) / timing.playbackRate);
134  }
135
136  function repeatedDuration(timing) {
137    return timing.duration * timing.iterations;
138  }
139
140  var PhaseNone = 0;
141  var PhaseBefore = 1;
142  var PhaseAfter = 2;
143  var PhaseActive = 3;
144
145  function calculatePhase(activeDuration, localTime, timing) {
146    if (localTime == null) {
147      return PhaseNone;
148    }
149    if (localTime < timing.delay) {
150      return PhaseBefore;
151    }
152    if (localTime >= timing.delay + activeDuration) {
153      return PhaseAfter;
154    }
155    return PhaseActive;
156  }
157
158  function calculateActiveTime(activeDuration, fillMode, localTime, phase, delay) {
159    switch (phase) {
160      case PhaseBefore:
161        if (fillMode == 'backwards' || fillMode == 'both')
162          return 0;
163        return null;
164      case PhaseActive:
165        return localTime - delay;
166      case PhaseAfter:
167        if (fillMode == 'forwards' || fillMode == 'both')
168          return activeDuration;
169        return null;
170      case PhaseNone:
171        return null;
172    }
173  }
174
175  function calculateScaledActiveTime(activeDuration, activeTime, startOffset, timing) {
176    return (timing.playbackRate < 0 ? activeTime - activeDuration : activeTime) * timing.playbackRate + startOffset;
177  }
178
179  function calculateIterationTime(iterationDuration, repeatedDuration, scaledActiveTime, startOffset, timing) {
180    if (scaledActiveTime === Infinity || scaledActiveTime === -Infinity || (scaledActiveTime - startOffset == repeatedDuration && timing.iterations && ((timing.iterations + timing.iterationStart) % 1 == 0))) {
181      return iterationDuration;
182    }
183
184    return scaledActiveTime % iterationDuration;
185  }
186
187  function calculateCurrentIteration(iterationDuration, iterationTime, scaledActiveTime, timing) {
188    if (scaledActiveTime === 0) {
189      return 0;
190    }
191    if (iterationTime == iterationDuration) {
192      return timing.iterationStart + timing.iterations - 1;
193    }
194    return Math.floor(scaledActiveTime / iterationDuration);
195  }
196
197  function calculateTransformedTime(currentIteration, iterationDuration, iterationTime, timing) {
198    var currentIterationIsOdd = currentIteration % 2 >= 1;
199    var currentDirectionIsForwards = timing.direction == 'normal' || timing.direction == (currentIterationIsOdd ? 'alternate-reverse' : 'alternate');
200    var directedTime = currentDirectionIsForwards ? iterationTime : iterationDuration - iterationTime;
201    var timeFraction = directedTime / iterationDuration;
202    return iterationDuration * timing.easing(timeFraction);
203  }
204
205  function calculateTimeFraction(activeDuration, localTime, timing) {
206    var phase = calculatePhase(activeDuration, localTime, timing);
207    var activeTime = calculateActiveTime(activeDuration, timing.fill, localTime, phase, timing.delay);
208    if (activeTime === null)
209      return null;
210    if (activeDuration === 0)
211      return phase === PhaseBefore ? 0 : 1;
212    var startOffset = timing.iterationStart * timing.duration;
213    var scaledActiveTime = calculateScaledActiveTime(activeDuration, activeTime, startOffset, timing);
214    var iterationTime = calculateIterationTime(timing.duration, repeatedDuration(timing), scaledActiveTime, startOffset, timing);
215    var currentIteration = calculateCurrentIteration(timing.duration, iterationTime, scaledActiveTime, timing);
216    return calculateTransformedTime(currentIteration, timing.duration, iterationTime, timing) / timing.duration;
217  }
218
219  shared.makeTiming = makeTiming;
220  shared.normalizeTimingInput = normalizeTimingInput;
221  shared.calculateActiveDuration = calculateActiveDuration;
222  shared.calculateTimeFraction = calculateTimeFraction;
223  shared.calculatePhase = calculatePhase;
224  shared.toTimingFunction = toTimingFunction;
225
226  if (WEB_ANIMATIONS_TESTING) {
227    testing.normalizeTimingInput = normalizeTimingInput;
228    testing.toTimingFunction = toTimingFunction;
229    testing.calculateActiveDuration = calculateActiveDuration;
230    testing.calculatePhase = calculatePhase;
231    testing.PhaseNone = PhaseNone;
232    testing.PhaseBefore = PhaseBefore;
233    testing.PhaseActive = PhaseActive;
234    testing.PhaseAfter = PhaseAfter;
235    testing.calculateActiveTime = calculateActiveTime;
236    testing.calculateScaledActiveTime = calculateScaledActiveTime;
237    testing.calculateIterationTime = calculateIterationTime;
238    testing.calculateCurrentIteration = calculateCurrentIteration;
239    testing.calculateTransformedTime = calculateTransformedTime;
240  }
241
242})(webAnimationsShared, webAnimationsTesting);
243