| <!DOCTYPE html> |
| <meta charset="UTF-8"> |
| <title>transform interpolation</title> |
| <link rel="help" href="https://drafts.csswg.org/css-transforms/#transform-property"> |
| <meta name="assert" content="transform supports animation as a transform list"> |
| <meta name="timeout" content="long"> |
| |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/css/support/interpolation-testcommon.js"></script> |
| |
| <style> |
| .target { |
| color: white; |
| width: 100px; |
| height: 100px; |
| background-color: black; |
| display: inline-block; |
| overflow: hidden; |
| } |
| .expected { |
| background-color: green; |
| } |
| .target > div { |
| width: 10px; |
| height: 10px; |
| display: inline-block; |
| background: orange; |
| margin: 1px; |
| } |
| .test { |
| overflow: hidden; |
| } |
| </style> |
| |
| <body> |
| <template id="target-template"> |
| <div></div> |
| </template> |
| </body> |
| |
| <script> |
| |
| // The default comparison function calls normalizeValue, which rounds |
| // everything to two decimal places, which isn't OK for the matrices |
| // that result from large perspective values. |
| const compareWithPerspective = (actual, expected) => { |
| // TODO: This RegExp should be more precise to capture only what is a |
| // valid float, and this code should be merged with other code doing |
| // the same thing, e.g., RoundMatrix in |
| // web-animations/animation-model/animation-types/property-list.js . |
| const matrixRegExp = /^matrix3d\(((?:(?:[-0-9.e]+), ){15}(?:[-0-9.]+))\)$/; |
| const actualMatch = actual.match(matrixRegExp); |
| const expectedMatch = expected.match(matrixRegExp); |
| assert_not_equals(actualMatch, null, `${actual} should be a matrix`); |
| assert_not_equals(expectedMatch, null, `${expected} should be a matrix`); |
| if (actualMatch === null || expectedMatch === null) { |
| return; |
| } |
| const actualArray = actualMatch[1].split(", ").map(Number); |
| const expectedArray = expectedMatch[1].split(", ").map(Number); |
| assert_equals(actualArray.length, 16); |
| assert_equals(expectedArray.length, 16); |
| |
| if (actualArray.length != expectedArray.length) { |
| return; |
| } |
| |
| for (let i in actualArray) { |
| const error = Math.abs((actualArray[i] - expectedArray[i])) / |
| Math.max(1e-6, |
| Math.min(Math.abs(expectedArray[i]), |
| Math.abs(actualArray[i]))); |
| assert_less_than(error, 1e-5, `comparing (at index ${i} actual value "${actual}" [${actualArray[i]}] and expected value "${expected}" [${expectedArray[i]}]`); |
| } |
| }; |
| |
| // The spec at |
| // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions |
| // requires that perspective be interpolated by decomposing the matrix |
| // (which is trivial for perspective) and then interpolating the pieces. |
| // The piece that's interpolated (the z part of the perspective array) |
| // contains the negative reciprocal of the argument to perspective(). |
| const interpolatePerspective = (from, to, progress) => { |
| return 1.0/((1.0 - progress) * (1.0/from) + progress * (1.0/to)); |
| }; |
| |
| // Perspective |
| test_interpolation({ |
| property: 'transform', |
| from: 'perspective(400px)', |
| to: 'perspective(500px)', |
| comparisonFunction: compareWithPerspective |
| }, [ |
| {at: -1, expect: `perspective(${interpolatePerspective(400, 500, -1)}px)`}, |
| {at: 0, expect: `perspective(${interpolatePerspective(400, 500, 0)}px)`}, |
| {at: 0.25, expect: `perspective(${interpolatePerspective(400, 500, 0.25)}px)`}, |
| {at: 0.75, expect: `perspective(${interpolatePerspective(400, 500, 0.75)}px)`}, |
| {at: 1, expect: `perspective(${interpolatePerspective(400, 500, 1)}px)`}, |
| {at: 2, expect: `perspective(${interpolatePerspective(400, 500, 2)}px)`}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'skewX(10rad) perspective(400px)', |
| to: 'skewX(20rad) perspective(500px)', |
| comparisonFunction: compareWithPerspective |
| }, [ |
| {at: -1, expect: `skewX(0rad) perspective(${interpolatePerspective(400, 500, -1)}px)`}, |
| {at: 0, expect: `skewX(10rad) perspective(${interpolatePerspective(400, 500, 0)}px)`}, |
| {at: 0.25, expect: `skewX(12.5rad) perspective(${interpolatePerspective(400, 500, 0.25)}px)`}, |
| {at: 0.75, expect: `skewX(17.5rad) perspective(${interpolatePerspective(400, 500, 0.75)}px)`}, |
| {at: 1, expect: `skewX(20rad) perspective(${interpolatePerspective(400, 500, 1)}px)`}, |
| {at: 2, expect: `skewX(30rad) perspective(${interpolatePerspective(400, 500, 2)}px)`}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'scaleZ(1) perspective(400px)', |
| to: 'scaleZ(2) perspective(500px)', |
| comparisonFunction: compareWithPerspective |
| }, [ |
| {at: -1, expect: `scaleZ(0) perspective(${interpolatePerspective(400, 500, -1)}px)`}, |
| {at: 0, expect: `scaleZ(1.0) perspective(${interpolatePerspective(400, 500, 0)}px)`}, |
| {at: 0.25, expect: `scaleZ(1.25) perspective(${interpolatePerspective(400, 500, 0.25)}px)`}, |
| {at: 0.75, expect: `scaleZ(1.75) perspective(${interpolatePerspective(400, 500, 0.75)}px)`}, |
| {at: 1, expect: `scaleZ(2) perspective(${interpolatePerspective(400, 500, 1)}px)`}, |
| {at: 2, expect: `scaleZ(3) perspective(${interpolatePerspective(400, 500, 2)}px)`}, |
| ]); |
| // Test that the transform identity function for perspective is perspective(none) |
| test_interpolation({ |
| property: 'transform', |
| from: 'scaleZ(2)', |
| to: 'scaleZ(2) perspective(500px)', |
| comparisonFunction: compareWithPerspective |
| }, [ |
| {at: -1, expect: `scaleZ(2)`}, |
| {at: 0, expect: `scaleZ(2)`}, |
| {at: 0.5, expect: `scaleZ(2) perspective(1000px)`}, |
| {at: 1, expect: `scaleZ(2) perspective(500px)`}, |
| {at: 2, expect: `scaleZ(2) perspective(250px)`}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'perspective(none)', |
| to: 'perspective(500px)', |
| }, [ |
| {at: -1, expect: `perspective(none)`}, |
| {at: 0, expect: `perspective(none)`}, |
| {at: 0.5, expect: `perspective(1000px)`}, |
| {at: 1, expect: `perspective(500px)`}, |
| {at: 2, expect: `perspective(250px)`}, |
| ]); |
| |
| // Rotate |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate(30deg)', |
| to: 'rotate(330deg)' |
| }, [ |
| {at: -1, expect: 'rotate(-270deg)'}, |
| {at: 0, expect: 'rotate(30deg)'}, |
| {at: 0.25, expect: 'rotate(105deg)'}, |
| {at: 0.75, expect: 'rotate(255deg)'}, |
| {at: 1, expect: 'rotate(330deg)'}, |
| {at: 2, expect: 'rotate(630deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotateX(0deg)', |
| to: 'rotateX(700deg)' |
| }, [ |
| {at: -1, expect: 'rotateX(-700deg)'}, |
| {at: 0, expect: 'rotateX(0deg)'}, |
| {at: 0.25, expect: 'rotateX(175deg)'}, |
| {at: 0.75, expect: 'rotateX(525deg)'}, |
| {at: 1, expect: 'rotateX(700deg)'}, |
| {at: 2, expect: 'rotateX(1400deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotateY(0deg)', |
| to: 'rotateY(800deg)' |
| }, [ |
| {at: -1, expect: 'rotateY(-800deg)'}, |
| {at: 0, expect: 'rotateY(0deg)'}, |
| {at: 0.25, expect: 'rotateY(200deg)'}, |
| {at: 0.75, expect: 'rotateY(600deg)'}, |
| {at: 1, expect: 'rotateY(800deg)'}, |
| {at: 2, expect: 'rotateY(1600deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotateZ(0deg)', |
| to: 'rotateZ(900deg)' |
| }, [ |
| {at: -1, expect: 'rotateZ(-900deg)'}, |
| {at: 0, expect: 'rotateZ(0deg)'}, |
| {at: 0.25, expect: 'rotateZ(225deg)'}, |
| {at: 0.75, expect: 'rotateZ(675deg)'}, |
| {at: 1, expect: 'rotateZ(900deg)'}, |
| {at: 2, expect: 'rotateZ(1800deg)'}, |
| ]); |
| // Interpolation is about a common axis if either endpoint has a rotation angle |
| // of zero. |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotateX(0deg)', |
| to: 'rotateY(900deg)' |
| }, [ |
| {at: -1, expect: 'rotateY(-900deg)'}, |
| {at: 0, expect: 'rotateY(0deg)'}, |
| {at: 0.25, expect: 'rotateY(225deg)'}, |
| {at: 0.75, expect: 'rotateY(675deg)'}, |
| {at: 1, expect: 'rotateY(900deg)'}, |
| {at: 2, expect: 'rotateY(1800deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotateY(900deg)', |
| to: 'rotateZ(0deg)' |
| }, [ |
| {at: -1, expect: 'rotateY(1800deg)'}, |
| {at: 0, expect: 'rotateY(900deg)'}, |
| {at: 0.25, expect: 'rotateY(675deg)'}, |
| {at: 0.75, expect: 'rotateY(225deg)'}, |
| {at: 1, expect: 'rotateY(0deg)'}, |
| {at: 2, expect: 'rotateY(-900deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate3d(7, 8, 9, 100deg)', |
| to: 'rotate3d(7, 8, 9, 260deg)' |
| }, [ |
| {at: -1, expect: 'rotate3d(7, 8, 9, -60deg)'}, |
| {at: 0, expect: 'rotate3d(7, 8, 9, 100deg)'}, |
| {at: 0.25, expect: 'rotate3d(7, 8, 9, 140deg)'}, |
| {at: 0.75, expect: 'rotate3d(7, 8, 9, 220deg)'}, |
| {at: 1, expect: 'rotate3d(7, 8, 9, 260deg)'}, |
| {at: 2, expect: 'rotate3d(7, 8, 9, 420deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate3d(7, 8, 9, 0deg)', |
| to: 'rotate3d(7, 8, 9, 450deg)' |
| }, [ |
| {at: -1, expect: 'rotate3d(7, 8, 9, -450deg)'}, |
| {at: 0, expect: 'rotate3d(7, 8, 9, 0deg)'}, |
| {at: 0.25, expect: 'rotate3d(7, 8, 9, 112.5deg)'}, |
| {at: 0.75, expect: 'rotate3d(7, 8, 9, 337.5deg)'}, |
| {at: 1, expect: 'rotate3d(7, 8, 9, 450deg)'}, |
| {at: 2, expect: 'rotate3d(7, 8, 9, 900deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate3d(0, 1, 0, 0deg)', |
| to: 'rotate3d(0, 1, 0, 450deg)' |
| }, [ |
| {at: -1, expect: 'rotate3d(0, 1, 0, -450deg)'}, |
| {at: 0, expect: 'rotate3d(0, 1, 0, 0deg)'}, |
| {at: 0.25, expect: 'rotate3d(0, 1, 0, 112.5deg)'}, |
| {at: 0.75, expect: 'rotate3d(0, 1, 0, 337.5deg)'}, |
| {at: 1, expect: 'rotate3d(0, 1, 0, 450deg)'}, |
| {at: 2, expect: 'rotate3d(0, 1, 0, 900deg)'}, |
| ]); |
| // Rotation is about a common axis if the axes are colinear. |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate3d(0, 1, 0, 0deg)', |
| to: 'rotate3d(0, 2, 0, 450deg)' |
| }, [ |
| {at: -1, expect: 'rotate3d(0, 1, 0, -450deg)'}, |
| {at: 0, expect: 'rotate3d(0, 1, 0, 0deg)'}, |
| {at: 0.25, expect: 'rotate3d(0, 1, 0, 112.5deg)'}, |
| {at: 0.75, expect: 'rotate3d(0, 1, 0, 337.5deg)'}, |
| {at: 1, expect: 'rotate3d(0, 1, 0, 450deg)'}, |
| {at: 2, expect: 'rotate3d(0, 1, 0, 900deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate3d(1, 1, 0, 90deg)', |
| to: 'rotate3d(0, 1, 1, 180deg)' |
| }, [ |
| {at: -1, expect: 'rotate3d(0.41, -0.41, -0.82, 120deg)'}, |
| {at: 0, expect: 'rotate3d(1, 1, 0, 90deg)'}, |
| {at: 0.25, expect: 'rotate3d(0.524083, 0.804261, 0.280178, 106.91deg)'}, |
| {at: 0.75, expect: 'rotate3d(0.163027, 0.774382, 0.611354, 153.99deg)'}, |
| {at: 1, expect: 'rotate3d(0, 1, 1, 180deg)'}, |
| {at: 2, expect: 'rotate3d(0.71, 0, -0.71, 90deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'none', |
| to: 'rotate(90deg)' |
| }, [ |
| {at: -1, expect: 'rotate(-90deg)'}, |
| {at: 0, expect: 'rotate(0deg)'}, |
| {at: 0.25, expect: 'rotate(22.5deg)'}, |
| {at: 0.75, expect: 'rotate(67.5deg)'}, |
| {at: 1, expect: 'rotate(90deg)'}, |
| {at: 2, expect: 'rotate(180deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotate(90deg)', |
| to: 'none' |
| }, [ |
| {at: -1, expect: 'rotate(180deg)'}, |
| {at: 0, expect: 'rotate(90deg)'}, |
| {at: 0.25, expect: 'rotate(67.5deg)'}, |
| {at: 0.75, expect: 'rotate(22.5deg)'}, |
| {at: 1, expect: 'rotate(0deg)'}, |
| {at: 2, expect: 'rotate(-90deg)'}, |
| ]); |
| test_interpolation({ |
| property: 'transform', |
| from: 'rotateX(0deg) rotateY(0deg) rotateZ(0deg)', |
| to: 'rotateX(700deg) rotateY(800deg) rotateZ(900deg)' |
| }, [ |
| {at: -1, expect: 'rotateX(-700deg) rotateY(-800deg) rotateZ(-900deg)'}, |
| {at: 0, expect: 'rotateX(0deg) rotateY(0deg) rotateZ(0deg)'}, |
| {at: 0.25, expect: 'rotateX(175deg) rotateY(200deg) rotateZ(225deg)'}, |
| {at: 0.75, expect: 'rotateX(525deg) rotateY(600deg) rotateZ(675deg)'}, |
| {at: 1, expect: 'rotateX(700deg) rotateY(800deg) rotateZ(900deg)'}, |
| {at: 2, expect: 'rotateX(1400deg) rotateY(1600deg) rotateZ(1800deg)'}, |
| ]); |
| </script> |