diff --git a/src/keyframe-interpolations.js b/src/keyframe-interpolations.js index 189d441..cab801e 100644 --- a/src/keyframe-interpolations.js +++ b/src/keyframe-interpolations.js @@ -25,7 +25,7 @@ }).forEach(function(interpolation) { var offsetFraction = fraction - interpolation.startOffset; var localDuration = interpolation.endOffset - interpolation.startOffset; - var scaledLocalTime = localDuration == 0 ? 0 : interpolation.easing(offsetFraction / localDuration); + var scaledLocalTime = localDuration == 0 ? 0 : interpolation.easingFunction(offsetFraction / localDuration); scope.apply(target, interpolation.property, interpolation.interpolation(scaledLocalTime)); }); } else { @@ -95,13 +95,12 @@ } } - var easing = keyframes[startIndex].easing; interpolations.push({ applyFrom: applyFrom, applyTo: applyTo, startOffset: keyframes[startIndex].offset, endOffset: keyframes[endIndex].offset, - easing: shared.toTimingFunction(easing ? easing : 'linear'), + easingFunction: shared.parseEasingFunction(keyframes[startIndex].easing), property: groupName, interpolation: scope.propertyInterpolation(groupName, keyframes[startIndex].value, diff --git a/src/normalize-keyframes.js b/src/normalize-keyframes.js index 0603fd0..241fd38 100644 --- a/src/normalize-keyframes.js +++ b/src/normalize-keyframes.js @@ -231,14 +231,22 @@ if (memberValue != null) { memberValue = Number(memberValue); if (!isFinite(memberValue)) - throw new TypeError('keyframe offsets must be numbers.'); + throw new TypeError('Keyframe offsets must be numbers.'); + if (memberValue < 0 || memberValue > 1) + throw new TypeError('Keyframe offsets must be between 0 and 1.'); } } else if (member == 'composite') { - throw { - type: DOMException.NOT_SUPPORTED_ERR, - name: 'NotSupportedError', - message: 'add compositing is not supported' - }; + if (memberValue == 'add' || memberValue == 'accumulate') { + throw { + type: DOMException.NOT_SUPPORTED_ERR, + name: 'NotSupportedError', + message: 'add compositing is not supported' + }; + } else if (memberValue != 'replace') { + throw new TypeError('Invalid composite mode ' + memberValue + '.'); + } + } else if (member == 'easing') { + memberValue = shared.normalizeEasing(memberValue); } else { memberValue = '' + memberValue; } @@ -246,6 +254,8 @@ } if (keyframe.offset == undefined) keyframe.offset = null; + if (keyframe.easing == undefined) + keyframe.easing = 'linear'; return keyframe; }); @@ -256,11 +266,7 @@ var offset = keyframes[i].offset; if (offset != null) { if (offset < previousOffset) { - throw { - code: DOMException.INVALID_MODIFICATION_ERR, - name: 'InvalidModificationError', - message: 'Keyframes are not loosely sorted by offset. Sort or specify offsets.' - }; + throw new TypeError('Keyframes are not loosely sorted by offset. Sort or specify offsets.'); } previousOffset = offset; } else { diff --git a/src/timing-utilities.js b/src/timing-utilities.js index 069e01b..511a18c 100644 --- a/src/timing-utilities.js +++ b/src/timing-utilities.js @@ -104,7 +104,7 @@ return this._direction; }, set easing(value) { - this._easingFunction = toTimingFunction(value); + this._easingFunction = parseEasingFunction(normalizeEasing(value)); this._setMember('easing', value); }, get easing() { @@ -224,32 +224,39 @@ var cubicBezierRe = new RegExp('cubic-bezier\\(' + numberString + ',' + numberString + ',' + numberString + ',' + numberString + '\\)'); var stepRe = /steps\(\s*(\d+)\s*,\s*(start|middle|end)\s*\)/; - function toTimingFunction(easing) { + function normalizeEasing(easing) { if (!styleForCleaning) { styleForCleaning = document.createElement('div').style; } styleForCleaning.animationTimingFunction = ''; styleForCleaning.animationTimingFunction = easing; - var validatedEasing = styleForCleaning.animationTimingFunction; - - if (validatedEasing == '' && isInvalidTimingDeprecated()) { + var normalizedEasing = styleForCleaning.animationTimingFunction; + if (normalizedEasing == '' && isInvalidTimingDeprecated()) { throw new TypeError(easing + ' is not a valid value for easing'); } + return normalizedEasing; + } - var cubicData = cubicBezierRe.exec(validatedEasing); + function parseEasingFunction(normalizedEasing) { + if (normalizedEasing == 'linear') { + return linear; + } + var cubicData = cubicBezierRe.exec(normalizedEasing); if (cubicData) { return cubic.apply(this, cubicData.slice(1).map(Number)); } - var stepData = stepRe.exec(validatedEasing); + var stepData = stepRe.exec(normalizedEasing); if (stepData) { return step(Number(stepData[1]), {'start': Start, 'middle': Middle, 'end': End}[stepData[2]]); } - var preset = presets[validatedEasing]; + var preset = presets[normalizedEasing]; if (preset) { return preset; } + // At this point none of our parse attempts succeeded; the easing is invalid. + // Fall back to linear in the interest of not crashing the page. return linear; - }; + } function calculateActiveDuration(timing) { return Math.abs(repeatedDuration(timing) / timing.playbackRate); @@ -345,11 +352,13 @@ shared.calculateActiveDuration = calculateActiveDuration; shared.calculateTimeFraction = calculateTimeFraction; shared.calculatePhase = calculatePhase; - shared.toTimingFunction = toTimingFunction; + shared.normalizeEasing = normalizeEasing; + shared.parseEasingFunction = parseEasingFunction; if (WEB_ANIMATIONS_TESTING) { testing.normalizeTimingInput = normalizeTimingInput; - testing.toTimingFunction = toTimingFunction; + testing.normalizeEasing = normalizeEasing; + testing.parseEasingFunction = parseEasingFunction; testing.calculateActiveDuration = calculateActiveDuration; testing.calculatePhase = calculatePhase; testing.PhaseNone = PhaseNone; diff --git a/test/js/keyframes.js b/test/js/keyframes.js index a9fb864..0b1f85e 100644 --- a/test/js/keyframes.js +++ b/test/js/keyframes.js @@ -45,7 +45,7 @@ suite('keyframes', function() { test('Normalize keyframes with some offsets not specified, but sorted by offset where specified. Some offsets are out of [0, 1] range.', function() { var normalizedKeyframes; - assert.doesNotThrow(function() { + assert.throws(function() { normalizedKeyframes = normalizeKeyframes([ {offset: -1}, {offset: 0}, @@ -55,11 +55,6 @@ suite('keyframes', function() { {offset: 2} ]); }); - assert.equal(normalizedKeyframes.length, 4); - assert.closeTo(normalizedKeyframes[0].offset, 0, 0.001); - assert.closeTo(normalizedKeyframes[1].offset, 0.5, 0.001); - assert.closeTo(normalizedKeyframes[2].offset, 0.75, 0.001); - assert.closeTo(normalizedKeyframes[3].offset, 1, 0.001); }); test('Normalize keyframes with some offsets not specified, but sorted by offset where specified. All specified offsets in [0, 1] range.', function() { @@ -151,14 +146,13 @@ suite('keyframes', function() { test('Normalize keyframes with invalid specified easing.', function() { var normalizedKeyframes; - assert.doesNotThrow(function() { + assert.throws(function() { normalizedKeyframes = normalizeKeyframes([ {left: '0px', easing: 'easy-peasy'}, {left: '10px'}, {left: '0px'} ]); }); - assert.equal(normalizedKeyframes[0].easing, 'easy-peasy'); }); test('Normalize keyframes where some properties are given non-string, non-number values.', function() { diff --git a/test/js/timing-utilities.js b/test/js/timing-utilities.js index 3e929cb..ac05b00 100644 --- a/test/js/timing-utilities.js +++ b/test/js/timing-utilities.js @@ -8,6 +8,11 @@ suite('timing-utilities', function() { assert.equal(calculateActiveDuration({duration: 1000, playbackRate: 4, iterations: 20}), 5000); assert.equal(calculateActiveDuration({duration: 500, playbackRate: 0.1, iterations: 300}), 1500000); }); + + function convertEasing(easing) { + return parseEasingFunction(normalizeEasing(easing)); + } + test('conversion of timing functions', function() { function assertTimingFunctionsEqual(tf1, tf2, message) { for (var i = 0; i <= 1; i += 0.1) { @@ -16,12 +21,12 @@ suite('timing-utilities', function() { } assertTimingFunctionsEqual( - toTimingFunction('ease-in-out'), - toTimingFunction('eAse\\2d iN-ouT'), + convertEasing('ease-in-out'), + convertEasing('eAse\\2d iN-ouT'), 'Should accept arbitrary casing and escape chararcters'); - var f = toTimingFunction('ease'); - var g = toTimingFunction('cubic-bezier(.25, 0.1, 0.25, 1.0)'); + var f = convertEasing('ease'); + var g = convertEasing('cubic-bezier(.25, 0.1, 0.25, 1.0)'); assertTimingFunctionsEqual(f, g, 'ease should map onto preset cubic-bezier'); assert.closeTo(f(0.1844), 0.2599, 0.001); assert.closeTo(g(0.1844), 0.2599, 0.001); @@ -30,12 +35,12 @@ suite('timing-utilities', function() { assert.equal(g(0), 0); assert.equal(g(1), 1); - f = toTimingFunction('cubic-bezier(0, 1, 1, 0)'); + f = convertEasing('cubic-bezier(0, 1, 1, 0)'); assert.closeTo(f(0.104), 0.3920, 0.001); function assertInvalidEasingThrows(easing) { assert.throws(function() { - toTimingFunction(easing); + convertEasing(easing); }, easing); } @@ -45,7 +50,7 @@ suite('timing-utilities', function() { assertInvalidEasingThrows('cubic-bezier(-1, 1, 1, 1)'); assertInvalidEasingThrows('cubic-bezier(1, 1, 1)'); - f = toTimingFunction('steps(10, end)'); + f = convertEasing('steps(10, end)'); assert.equal(f(0), 0); assert.equal(f(0.09), 0); assert.equal(f(0.1), 0.1); diff --git a/test/web-platform-tests-expectations.js b/test/web-platform-tests-expectations.js index 01115ff..a950ced 100644 --- a/test/web-platform-tests-expectations.js +++ b/test/web-platform-tests-expectations.js @@ -17,18 +17,7 @@ module.exports = { 'Element.animate() creates an Animation object': 'assert_equals: Returned object is an Animation expected "[object Animation]" but got "[object Object]"', - 'Element.animate() does not accept keyframes not loosely sorted by offset': - 'assert_throws: function "function () {\n"use strict";\n\n div.animate(subtest.input, 2000);\n }" threw object "[object Object]" ("InvalidModificationError") expected object "[object Object]" ("TypeError")', - - 'Element.animate() does not accept keyframes with an invalid composite value': - 'assert_throws: function "function () {\n"use strict";\n\n div.animate(subtest.input, 2000);\n }" threw object "[object Object]" ("NotSupportedError") expected object "[object Object]" ("TypeError")', - - 'Element.animate() does not accept keyframes with an out-of-bounded negative offset': - 'assert_throws: function "function () {\n"use strict";\n\n div.animate(subtest.input, 2000);\n }" did not throw', - - 'Element.animate() does not accept keyframes with an out-of-bounded positive offset': - 'assert_throws: function "function () {\n"use strict";\n\n div.animate(subtest.input, 2000);\n }" did not throw', - + // Seems to be a bug in Firefox 47? The TypeError is thrown but disappears by the time it bubbles up to assert_throws(). 'Element.animate() does not accept property-indexed keyframes with an invalid easing value': 'assert_throws: function "function () {\n"use strict";\n\n div.animate(subtest.input, 2000);\n }" did not throw', },