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