blob: 2586ff3d4a577b9c4b43a2841884c6f490d7ccf3 [file] [log] [blame]
<!DOCTYPE html>
<meta charset="UTF-8">
<title>transform-matrix composition</title>
<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#ctm">
<meta name="assert" content="transform-matrix supports animation as a transform list">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/css/support/interpolation-testcommon.js"></script>
<body>
<script>
// For matrix and matrix3d, addition is defined as concatenation whilst
// accumulation works by decomposing the matrix and then accumulating the
// decomposed functions. We can therefore test the difference between the
// two by mixing functions such that a naive multiplication would look
// different than the accumulation behavior.
//
// Note that due to the complexities of decomposition the test space here is
// huge; we cover some basic cases and hope that the tests for the individual
// functions provide a lot of the remaining coverage.
// Creates a matrix3d function, encoding the passed rotation and translation.
// Note that the translate will not be affected by the rotation.
function create3dMatrix(x, y, z, radians, translateX) {
// Normalize the rotation axes.
const length = Math.sqrt(x*x + y*y + z*z);
x /= length;
y /= length;
z /= length;
const sc = Math.sin(radians / 2) * Math.cos(radians / 2);
const sq = Math.sin(radians / 2) * Math.sin(radians / 2);
// https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined
// https://drafts.csswg.org/css-transforms-2/#Translate3dDefined
return 'matrix3d(' + [
1 - 2 * (y*y + z*z) * sq,
2 * (x * y * sq + z * sc),
2 * (x * z * sq - y * sc),
0,
2 * (x * y * sq - z * sc),
1 - 2 * (x*x + z*z) * sq,
2 * (y * z * sq + x * sc),
0,
2 * (x * z * sq + y * sc),
2 * (y * z * sq - x * sc),
1 - 2 * (x*x + y*y) * sq,
0,
translateX, 0, 0, 1].join() + ')';
}
// ------------ Addition tests --------------
test_composition({
property: 'transform',
// translateX(100px) rotate(90deg)
underlying: 'matrix(0, 1, -1, 0, 100, 0)',
// translateX(100px)
addFrom: 'matrix(1, 0, 0, 1, 100, 0)',
// translateX(200px)
addTo: 'matrix(1, 0, 0, 1, 200, 0)',
}, [
{at: -0.5, expect: 'matrix(0, 1, -1, 0, 100, 50)'},
{at: 0, expect: 'matrix(0, 1, -1, 0, 100, 100)'},
{at: 0.25, expect: 'matrix(0, 1, -1, 0, 100, 125)'},
{at: 0.5, expect: 'matrix(0, 1, -1, 0, 100, 150)'},
{at: 0.75, expect: 'matrix(0, 1, -1, 0, 100, 175)'},
{at: 1, expect: 'matrix(0, 1, -1, 0, 100, 200)'},
{at: 1.5, expect: 'matrix(0, 1, -1, 0, 100, 250)'},
]);
test_composition({
property: 'transform',
// translateX(100px) rotate3d(1, 1, 0, 45deg)
underlying: create3dMatrix(1, 1, 0, Math.PI / 4, 100),
// translateX(100px)
addFrom: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 100, 0, 0, 1)',
// translateX(200px)
addTo: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 200, 0, 0, 1)',
}, [
// matrix3ds are hard to read; these are the decomposed forms for clarity
{at: -0.5, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(50px)'},
{at: 0, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(100px)'},
{at: 0.25, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(125px)'},
{at: 0.5, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(150px)'},
{at: 0.75, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(175px)'},
{at: 1, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(200px)'},
{at: 1.5, expect: 'translateX(100px) rotate3d(1, 1, 0, 45deg) translateX(250px)'},
]);
// Addition of non-invertible matrices is still defined as concatenation so
// includes the underlying value.
test_composition({
property: 'transform',
// Non-invertible.
underlying: 'matrix(1, 1, 0, 0, 0, 100)',
// translateX(100px)
addFrom: 'matrix(1, 0, 0, 1, 100, 0)',
// translateX(200px)
addTo: 'matrix(1, 0, 0, 1, 200, 0)',
}, [
{at: -0.5, expect: 'matrix(1, 1, 0, 0, 100, 200)'},
{at: 0, expect: 'matrix(1, 1, 0, 0, 100, 200)'},
{at: 0.25, expect: 'matrix(1, 1, 0, 0, 100, 200)'},
{at: 0.5, expect: 'matrix(1, 1, 0, 0, 200, 300)'},
{at: 0.75, expect: 'matrix(1, 1, 0, 0, 200, 300)'},
{at: 1, expect: 'matrix(1, 1, 0, 0, 200, 300)'},
{at: 1.5, expect: 'matrix(1, 1, 0, 0, 200, 300)'},
]);
test_composition({
property: 'transform',
// translateX(100px)
underlying: 'matrix(1, 0, 0, 1, 100, 0)',
// Non-invertible
addFrom: 'matrix(1, 1, 0, 0, 0, 100)',
// translateX(200px)
addTo: 'matrix(1, 0, 0, 1, 200, 0)',
}, [
{at: -0.5, expect: 'matrix(1, 1, 0, 0, 100, 100)'},
{at: 0, expect: 'matrix(1, 1, 0, 0, 100, 100)'},
{at: 0.25, expect: 'matrix(1, 1, 0, 0, 100, 100)'},
{at: 0.5, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
{at: 0.75, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
{at: 1, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
{at: 1.5, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
]);
// ------------ Accumulation tests --------------
test_composition({
property: 'transform',
// translateX(100px) rotate(90deg)
underlying: 'matrix(0, 1, -1, 0, 100, 0)',
// translateX(100px)
accumulateFrom: 'matrix(1, 0, 0, 1, 100, 0)',
// translateX(200px)
accumulateTo: 'matrix(1, 0, 0, 1, 200, 0)',
}, [
{at: -0.5, expect: 'matrix(0, 1, -1, 0, 150, 0)'},
{at: 0, expect: 'matrix(0, 1, -1, 0, 200, 0)'},
{at: 0.25, expect: 'matrix(0, 1, -1, 0, 225, 0)'},
{at: 0.5, expect: 'matrix(0, 1, -1, 0, 250, 0)'},
{at: 0.75, expect: 'matrix(0, 1, -1, 0, 275, 0)'},
{at: 1, expect: 'matrix(0, 1, -1, 0, 300, 0)'},
{at: 1.5, expect: 'matrix(0, 1, -1, 0, 350, 0)'},
]);
test_composition({
property: 'transform',
// translateX(100px) rotate3d(1, 1, 0, 45deg)
underlying: create3dMatrix(1, 1, 0, Math.PI / 4, 100),
// translateX(100px)
accumulateFrom: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 100, 0, 0, 1)',
// translateX(200px)
accumulateTo: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 200, 0, 0, 1)',
}, [
// matrix3ds are hard to read; these are the decomposed forms for clarity
{at: -0.5, expect: 'translateX(150px) rotate3d(1, 1, 0, 45deg)'},
{at: 0, expect: 'translateX(200px) rotate3d(1, 1, 0, 45deg)'},
{at: 0.25, expect: 'translateX(225px) rotate3d(1, 1, 0, 45deg)'},
{at: 0.5, expect: 'translateX(250px) rotate3d(1, 1, 0, 45deg)'},
{at: 0.75, expect: 'translateX(275px) rotate3d(1, 1, 0, 45deg)'},
{at: 1, expect: 'translateX(300px) rotate3d(1, 1, 0, 45deg)'},
{at: 1.5, expect: 'translateX(350px) rotate3d(1, 1, 0, 45deg)'},
]);
// Accumulation of non-invertible matrices falls back to replace behavior.
test_composition({
property: 'transform',
// Non-invertible.
underlying: 'matrix(1, 1, 0, 0, 0, 100)',
// translateX(100px)
accumulateFrom: 'matrix(1, 0, 0, 1, 100, 0)',
// translateX(200px)
accumulateTo: 'matrix(1, 0, 0, 1, 200, 0)',
}, [
{at: -0.5, expect: 'matrix(1, 0, 0, 1, 50, 0)'},
{at: 0, expect: 'matrix(1, 0, 0, 1, 100, 0)'},
{at: 0.25, expect: 'matrix(1, 0, 0, 1, 125, 0)'},
{at: 0.5, expect: 'matrix(1, 0, 0, 1, 150, 0)'},
{at: 0.75, expect: 'matrix(1, 0, 0, 1, 175, 0)'},
{at: 1, expect: 'matrix(1, 0, 0, 1, 200, 0)'},
{at: 1.5, expect: 'matrix(1, 0, 0, 1, 250, 0)'},
]);
test_composition({
property: 'transform',
// translateX(100px)
underlying: 'matrix(1, 0, 0, 1, 100, 0)',
// Non-invertible
accumulateFrom: 'matrix(1, 1, 0, 0, 0, 100)',
// translateX(200px)
accumulateTo: 'matrix(1, 0, 0, 1, 200, 0)',
}, [
{at: -0.5, expect: 'matrix(1, 1, 0, 0, 0, 100)'},
{at: 0, expect: 'matrix(1, 1, 0, 0, 0, 100)'},
{at: 0.25, expect: 'matrix(1, 1, 0, 0, 0, 100)'},
{at: 0.5, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
{at: 0.75, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
{at: 1, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
{at: 1.5, expect: 'matrix(1, 0, 0, 1, 300, 0)'},
]);
</script>
</body>