summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/css-transforms/animation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/css/css-transforms/animation
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/css/css-transforms/animation')
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/backface-visibility-no-interpolation.html15
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation-ref.html27
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation.html41
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/composited-transform.html38
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/list-interpolation.html208
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/matrix-interpolation.html36
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/perspective-composition.html65
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/perspective-interpolation.html116
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/perspective-origin-interpolation.html107
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg-ref.html26
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg.html42
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001-ref.html19
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001.html38
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-composition.html227
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-interpolation.html250
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent-ref.html54
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent.html74
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg-ref.html26
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg.html41
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/scale-composition.html101
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/scale-interpolation.html242
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/support/transform-interpolation-reftests.js193
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-composition.html86
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-001.html325
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-002.html151
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-003.html128
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-004.html187
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-005.html268
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-006.html100
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-animated-ref.html11
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-computed-value.html90
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-inline-value.html80
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-matrix.html12
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-perspective.html12
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-ref.html11
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate-slerp.html13
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate.html13
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-scale.html12
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-skew.html24
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate-em.html12
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate.html12
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-verify-reftests.html35
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-matrix-composition.html208
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-origin-interpolation.html125
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-perspective-composition.html67
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-rotate-composition.html164
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-scale-composition.html129
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-skew-composition.html124
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/transform-translate-composition.html147
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg-ref.html26
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg.html41
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/translate-composition.html147
-rw-r--r--testing/web-platform/tests/css/css-transforms/animation/translate-interpolation.html271
53 files changed, 5017 insertions, 0 deletions
diff --git a/testing/web-platform/tests/css/css-transforms/animation/backface-visibility-no-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/backface-visibility-no-interpolation.html
new file mode 100644
index 0000000000..b7b9f2d5f0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/backface-visibility-no-interpolation.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/w3c/csswg-drafts/issues/4441">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<body>
+<script>
+test_no_interpolation({
+ property: 'backface-visibility',
+ from: 'initial',
+ to: 'hidden'
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation-ref.html b/testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation-ref.html
new file mode 100644
index 0000000000..6610c6f68b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation-ref.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>CSS Test: translate webgl canvas in an animation via set current time (ref).</title>
+<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org"/>
+<link rel="help" href="https://www.w3.org/TR/css-transforms-1/#funcdef-transform-translate"/>
+<script src="/common/reftest-wait.js"></script>
+
+<style>
+canvas {
+ will-change: transform;
+ transform: translate(150px);
+}
+</style>
+
+<canvas id="canvas" width="150" height="150"></canvas>
+
+<script>
+async function runReference() {
+ const gl = canvas.getContext("webgl");
+ gl.clearColor(0.0, 1.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ requestAnimationFrame(takeScreenshot);
+}
+onload = () => requestAnimationFrame(() => requestAnimationFrame(runReference));
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation.html b/testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation.html
new file mode 100644
index 0000000000..e1138a87e4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/canvas-webgl-translate-in-animation.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>CSS Test: translate webgl canvas in an animation via set current time.</title>
+<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org"/>
+<link rel="help" href="https://www.w3.org/TR/css-transforms-1/#funcdef-transform-translate"/>
+<link rel="match" href="canvas-webgl-translate-in-animation-ref.html"/>
+<meta name="assert" content="canvas is translated by half the total distance"/>
+<script src="/common/reftest-wait.js"></script>
+
+<style>
+@keyframes move {
+ to { transform: translate(300px); }
+}
+
+canvas {
+ will-change: transform;
+ animation: move;
+ animation-duration: 1s;
+ animation-timing-function: linear;
+ animation-play-state: paused;
+}
+</style>
+
+<canvas id="canvas" width="150" height="150"></canvas>
+
+<script>
+async function runTest() {
+ const gl = canvas.getContext("webgl");
+ gl.clearColor(0.0, 1.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ requestAnimationFrame(() => {
+ document.getAnimations().forEach((animation) => {
+ animation.currentTime = 500;
+ });
+ takeScreenshot();
+ });
+}
+onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest));
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/composited-transform.html b/testing/web-platform/tests/css/css-transforms/animation/composited-transform.html
new file mode 100644
index 0000000000..182321b9d3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/composited-transform.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Composition of transform animations</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#combining-transform-lists">
+<meta name="assert" content="transform animations should composite correctly">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="target"><div>
+
+<script>
+test(() => {
+ var anim1 = target.animate(
+ { transform: [ 'translateX(0)', 'translateX(100px)' ]},
+ 1000
+ );
+ var anim2 = target.animate(
+ { transform: [ 'translateY(0)', 'translateY(100px)' ]},
+ {duration: 1000, composite: 'add'}
+ );
+
+ anim1.pause();
+ anim2.pause();
+
+ anim1.currentTime = 200;
+ anim2.currentTime = 800;
+
+ // The computation here should be:
+ // underlying_value = 'translateX(0)' --> 'translateX(100px)' @ 0.2
+ // = 'translateX(20px)'
+ // final_value = 0.2 * ('translateX(20px) translateY(0)') +
+ // 0.8 * ('translateX(20px) translateY(100px)')
+ // = 'translateX(20px) translateY(80px)'
+ // = 'matrix(1, 0, 0, 1, 20, 80)'
+ assert_equals(getComputedStyle(target).transform, 'matrix(1, 0, 0, 1, 20, 80)')
+}, 'An additive transform animation on-top of a replace transform animation ' +
+ 'should composite correctly');
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/list-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/list-interpolation.html
new file mode 100644
index 0000000000..85701e9efa
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/list-interpolation.html
@@ -0,0 +1,208 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Transform list interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms">
+<meta name="assert" content="Interpolation of transform function lists is performed as follows">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'none',
+ to: 'none',
+ },
+ [{ at: 0.25, expect: 'none' }],
+ 'none -> none'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'none',
+ to: 'translate(200px) rotate(720deg)',
+ },
+ [{ at: 0.25, expect: 'translate(50px) rotate(180deg)' }],
+ 'none -> something'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'translate(200px) rotate(720deg)',
+ to: 'none',
+ },
+ [{ at: 0.25, expect: 'translate(150px) rotate(540deg)' }],
+ 'something -> none'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'translate(100px)',
+ to: 'translate(200px) rotate(720deg)',
+ },
+ [{ at: 0.25, expect: 'translate(125px) rotate(180deg)' }],
+ 'Mismatched lengths (from is shorter), common part matches'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'translate(100px) rotate(720deg)',
+ to: 'translate(200px)',
+ },
+ [{ at: 0.25, expect: 'translate(125px) rotate(540deg)' }],
+ 'Mismatched lengths (to is shorter), common part matches'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'scale(2) rotate(360deg) translate(100px) matrix(1, 0, 0, 1, 100, 0) skew(0deg)',
+ to: 'scale(3) rotate(1080deg) translate(200px) matrix(1, 0, 0, 1, 0, 200) skew(720deg)',
+ },
+ [
+ {
+ at: 0.25,
+ expect: 'scale(2.25) rotate(540deg) translate(125px) matrix(1, 0, 0, 1, 75, 50) skew(180deg)',
+ },
+ ],
+ 'Perfect match'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'translateX(100px) scaleX(3) translate(500px) scale(2)',
+ to: 'translateY(200px) scale(5) translateX(100px) scaleY(3)',
+ },
+ [{ at: 0.25, expect: 'translate(75px, 50px) scale(3.5, 2) translate(400px, 0px) scale(1.75, 2.25)' }],
+ 'Matches on primitives'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotateX(90deg) translateX(100px)',
+ to: 'rotate3d(50, 0, 0, 180deg) translateY(200px)',
+ },
+ [{ at: 0.25, expect: 'rotateX(112.5deg) translate(75px, 50px)' }],
+ 'Match on rotation vector'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotateX(90deg) translateX(100px)',
+ to: 'rotateY(0deg) translateY(200px)',
+ },
+ [{ at: 0.25, expect: 'rotateX(67.5deg) translate(75px, 50px)' }],
+ 'Match on rotation due to 0deg angle'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotate3d(1, 1, 1, -60deg) translateX(100px)',
+ to: 'rotate3d(2, 2, 2, 60deg) translateY(200px)',
+ }, [{ at: 0.25, expect: 'rotate3d(1, 1, 1, -30deg) translate(75px, 50px)' }],
+ 'Match on rotation using collinear rotation axes'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotate3d(1, 0, 0, 360deg) translateX(100px)',
+ to: 'rotate3d(0, 1, 0, -720deg) translateY(200px)',
+ }, [{ at: 0.25, expect: 'rotate3d(0, 0, 1, 0deg) translate(75px, 50px)' }],
+ 'Match on rotation with spherical interpolation'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotate(0deg) translate(100px)',
+ to: 'rotate(720deg) scale(2) translate(200px)',
+ },
+ [{ at: 0.25, expect: 'rotate(180deg) matrix(1.25, 0, 0, 1.25, 175, 0)' }],
+ 'Common prefix'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'scale(2) rotate(0deg) translate(100px)',
+ to: 'rotate(720deg) scale(2) translate(200px)',
+ },
+ [{ at: 0.25, expect: 'matrix(2, 0, 0, 2, 250, 0)' }],
+ 'Complete mismatch (except length)'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'scale(2) rotate(0deg)',
+ to: 'rotate(720deg) scale(2) translate(200px)',
+ },
+ [{ at: 0.25, expect: 'matrix(2, 0, 0, 2, 100, 0)' }],
+ 'Complete mismatch including length'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotate(0deg) scaleX(1)',
+ to: 'rotate(720deg) translateX(0px) scaleX(2)'
+ },
+ [{at: 0.25, expect: 'rotate(180deg) matrix(1.25, 0, 0, 1, 0, 0)'}],
+ 'Mismatched lengths (from is shorter), partial match'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotate(720deg) translateX(0px) scaleX(2)',
+ to: 'rotate(0deg) scaleX(1)'
+ },
+ [{at: 0.25, expect: 'rotate(540deg) matrix(1.75, 0, 0, 1, 0, 0)'}],
+ 'Mismatched lengths (to is shorter), partial match'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'scaleX(-3) scaleY(2)',
+ to: 'scaleY(-3) translateX(0px) scaleX(2)'
+ },
+ [{at: 0.25, expect: 'scale(-2, 0) matrix(1.25, 0, 0, 1.75, 0, 0)'}],
+ 'Mismatched lengths (from is shorter), partial match on primitive'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'scaleY(-3) translateX(0px) scaleX(2)',
+ to: 'scaleX(-3) scaleY(2)'
+ },
+ [{at: 0.25, expect: 'scale(0, -2) matrix(1.75, 0, 0, 1.25, 0, 0)'}],
+ 'Mismatched lengths (to is shorter), partial match on primitive'
+);
+
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'scaleY(-3) translateX(0px)',
+ to: 'scaleX(-3) scaleY(2)'
+ },
+ [{at: 0.25, expect: 'scale(0, -2) matrix(1, 0, 0, 1.25, 0, 0)'}],
+ 'Common prefix on primitive'
+);
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/matrix-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/matrix-interpolation.html
new file mode 100644
index 0000000000..a326e7c92f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/matrix-interpolation.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Matrix interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#interpolation-of-3d-matrices">
+<meta name="assert" content="When interpolating between two matrices, each matrix is decomposed into the corresponding translation, rotation, scale, skew and (for a 3D matrix) perspective values">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+<body>
+<script>
+// Test interpolation of quaternions when the dot product is -1.
+//
+// We need to be particularly careful not to use a rotate function with a zero
+// angle since the handling of zero angle rotations may change in future as per:
+//
+// https://github.com/w3c/csswg-drafts/issues/3236
+//
+// For rotateY(360deg) we should get a quaternion of:
+// [ 0, sin(2 * PI / 2), 0, cos(2 * PI / 2) ]
+// = [ 0, 0, 0, -1 ]
+//
+// For rotateX(720deg) we should get a quaternion of:
+// [ 0, 0, sin(4 * PI / 2), cos(4 * PI / 2) ]
+// = [ 0, 0, 0, 1 ]
+//
+// Dot product = 0 * 0 + 0 * 0 + 0 * 0 + 1 * -1 = -1
+test_interpolation(
+ {
+ property: 'transform',
+ from: 'rotateY(360deg)',
+ to: 'rotateX(720deg)',
+ },
+ [{ at: 0.5, expect: 'matrix(1, 0, 0, 1, 0, 0)' }]
+);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/perspective-composition.html b/testing/web-platform/tests/css/css-transforms/animation/perspective-composition.html
new file mode 100644
index 0000000000..6fa745d6ed
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/perspective-composition.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title> perspective composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-perspective">
+<meta name="assert" content="perspective supports animation">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<body>
+<script>
+test_composition({
+ property: 'perspective',
+ underlying: '50px',
+ addFrom: '100px',
+ addTo: '200px',
+}, [
+ {at: -0.3, expect: '120px'},
+ {at: 0, expect: '150px'},
+ {at: 0.5, expect: '200px'},
+ {at: 1, expect: '250px'},
+ {at: 1.5, expect: '300px'},
+]);
+
+test_composition({
+ property: 'perspective',
+ underlying: '100px',
+ addFrom: '10px',
+ addTo: '2px',
+}, [
+ {at: -0.5, expect: '114px'},
+ {at: 0, expect: '110px'},
+ {at: 0.5, expect: '106px'},
+ {at: 1, expect: '102px'},
+ {at: 1.5, expect: '98px'}, // Value clamping should happen after composition.
+]);
+
+test_composition({
+ property: 'perspective',
+ underlying: '50px',
+ addFrom: '100px',
+ replaceTo: '200px',
+}, [
+ {at: -0.3, expect: '135px'},
+ {at: 0, expect: '150px'},
+ {at: 0.5, expect: '175px'},
+ {at: 1, expect: '200px'},
+ {at: 1.5, expect: '225px'},
+]);
+
+test_composition({
+ property: 'perspective',
+ underlying: '100px',
+ addFrom: '100px',
+ addTo: 'none',
+}, [
+ {at: -0.3, expect: '200px'},
+ {at: 0, expect: '200px'},
+ {at: 0.5, expect: 'none'},
+ {at: 1, expect: 'none'},
+ {at: 1.5, expect: 'none'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/perspective-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/perspective-interpolation.html
new file mode 100644
index 0000000000..d3f165db22
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/perspective-interpolation.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title> perspective interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-perspective">
+<meta name="assert" content="perspective supports animation">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<style>
+.parent {
+ perspective: 30px;
+}
+.target {
+ perspective: 10px;
+}
+.transformed {
+ width: 50px;
+ height: 50px;
+ background: black;
+ transform: rotateY(45deg);
+}
+.expected .transformed {
+ background: green;
+}
+.expected {
+ position: relative;
+ left: -50px;
+ opacity: 0.75;
+}
+</style>
+<body>
+<template id="target-template">
+<div><div class="transformed"></div></div>
+</template>
+<script>
+test_interpolation({
+ property: 'perspective',
+ from: neutralKeyframe,
+ to: '20px',
+}, [
+ {at: -20, expect: '0px'},
+ {at: -1, expect: '0px'},
+ {at: -0.3, expect: '7px'},
+ {at: 0, expect: '10px'},
+ {at: 0.3, expect: '13px'},
+ {at: 0.6, expect: '16px'},
+ {at: 1, expect: '20px'},
+ {at: 1.5, expect: '25px'},
+]);
+
+test_no_interpolation({
+ property: 'perspective',
+ from: 'initial',
+ to: '20px',
+});
+
+test_interpolation({
+ property: 'perspective',
+ from: 'inherit',
+ to: '20px',
+}, [
+ {at: -20, expect: '230px'},
+ {at: -1, expect: '40px'},
+ {at: -0.3, expect: '33px'},
+ {at: 0, expect: '30px'},
+ {at: 0.3, expect: '27px'},
+ {at: 0.6, expect: '24px'},
+ {at: 1, expect: '20px'},
+ {at: 1.5, expect: '15px'},
+]);
+
+test_no_interpolation({
+ property: 'perspective',
+ from: 'unset',
+ to: '20px',
+});
+
+test_interpolation({
+ property: 'perspective',
+ from: '50px',
+ to: '100px',
+}, [
+ {at: -20, expect: '0px'}, // perspective does not accept negative values
+ {at: -1, expect: '0px'}, // perspective does not accept negative values
+ {at: -0.3, expect: '35px'},
+ {at: 0, expect: '50px'},
+ {at: 0.3, expect: '65px'},
+ {at: 0.6, expect: '80px'},
+ {at: 1, expect: '100px'},
+ {at: 1.5, expect: '125px'},
+]);
+
+test_interpolation({
+ property: 'perspective',
+ from: '0px', // Test that there's no special handling of 0px, as for perspective()
+ to: '10px',
+}, [
+ {at: -20, expect: '0px'}, // perspective does not accept negative values
+ {at: -1, expect: '0px'}, // perspective does not accept negative values
+ {at: -0.3, expect: '0px'},
+ {at: 0, expect: '0px'},
+ {at: 0.3, expect: '3px'},
+ {at: 0.6, expect: '6px'},
+ {at: 1, expect: '10px'},
+ {at: 1.5, expect: '15px'},
+]);
+
+test_no_interpolation({
+ property: 'perspective',
+ from: '50px',
+ to: 'none',
+});
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/perspective-origin-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/perspective-origin-interpolation.html
new file mode 100644
index 0000000000..0a1e74cb8e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/perspective-origin-interpolation.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>perspective-origin interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#perspective-origin-property">
+<meta name="assert" content="perspective-origin supports animation">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<style>
+.parent {
+ perspective-origin: 30px 10px;
+}
+.target {
+ display: inline-block;
+ perspective: 50;
+ margin-top: 50px;
+ margin-bottom: 25px;
+ perspective-origin: 10px 30px;
+}
+.transformed {
+ width: 50px;
+ height: 50px;
+ background: black;
+ transform: rotateY(45deg);
+}
+.expected .transformed {
+ background: green;
+}
+.expected {
+ position: relative;
+ left: -50px;
+ opacity: 0.75;
+}
+</style>
+<body>
+<template id="target-template">
+<div><div class="transformed"></div></div>
+</template>
+<script>
+test_interpolation({
+ property: 'perspective-origin',
+ from: neutralKeyframe,
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '7px 33px'},
+ {at: 0, expect: '10px 30px'},
+ {at: 0.3, expect: '13px 27px'},
+ {at: 0.6, expect: '16px 24px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '25px 15px'},
+]);
+
+test_interpolation({
+ property: 'perspective-origin',
+ from: 'initial',
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '26.5px 26.5px'},
+ {at: 0, expect: '25px 25px'},
+ {at: 0.3, expect: '23.5px 23.5px'},
+ {at: 0.6, expect: '22px 22px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '17.5px 17.5px'},
+]);
+
+test_interpolation({
+ property: 'perspective-origin',
+ from: 'inherit',
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '33px 7px'},
+ {at: 0, expect: '30px 10px'},
+ {at: 0.3, expect: '27px 13px'},
+ {at: 0.6, expect: '24px 16px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '15px 25px'},
+]);
+
+test_interpolation({
+ property: 'perspective-origin',
+ from: 'unset',
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '26.5px 26.5px'},
+ {at: 0, expect: '25px 25px'},
+ {at: 0.3, expect: '23.5px 23.5px'},
+ {at: 0.6, expect: '22px 22px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '17.5px 17.5px'},
+]);
+
+test_interpolation({
+ property: 'perspective-origin',
+ from: '0% 50%',
+ to: '100% 150%'
+}, [
+ {at: -0.3, expect: '-30% 20%'},
+ {at: 0, expect: '0% 50%'},
+ {at: 0.3, expect: '30% 80%'},
+ {at: 0.6, expect: '60% 110%'},
+ {at: 1, expect: '100% 150%'},
+ {at: 1.5, expect: '150% 200%'}
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg-ref.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg-ref.html
new file mode 100644
index 0000000000..fda02db187
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Animating the "rotate" property on an SVG element</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+
+<style>
+
+svg {
+ width: 400px;
+ height: 400px;
+}
+
+rect {
+ width: 100px;
+ height: 100px;
+ transform-origin: 100px 100px;
+ rotate: 180deg;
+}
+
+</style>
+</head>
+<body>
+<svg><rect></rect></svg>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg.html
new file mode 100644
index 0000000000..fed931b00a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-on-svg.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<title>Animating the "rotate" property on an SVG element</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+<link rel="match" href="rotate-animation-on-svg-ref.html">
+
+<style>
+
+@keyframes rotate-animation {
+ from { rotate: 0; }
+ to { rotate: 180deg; }
+}
+
+svg {
+ width: 400px;
+ height: 400px;
+ overflow: visible;
+}
+
+rect {
+ width: 100px;
+ height: 100px;
+ transform-origin: 100px 100px;
+ animation: rotate-animation 1ms linear forwards;
+}
+
+</style>
+</head>
+<body>
+<svg><rect></rect></svg>
+
+<script>
+
+(async function() {
+ await Promise.all(document.getAnimations().map(animation => animation.finished));
+ document.documentElement.classList.remove("reftest-wait");
+})();
+
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001-ref.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001-ref.html
new file mode 100644
index 0000000000..0dd93d3f7b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>CSS Test (Transforms): Transform and perspective with w negative</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+
+<style>
+
+div {
+ width: 100px;
+ height: 100px;
+ transform: rotateY(44deg);
+ background: fuchsia;
+ transform-origin: 100px 0;
+ will-change: transform;
+}
+
+</style>
+
+<div></div>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001.html
new file mode 100644
index 0000000000..9d330e1573
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-animation-with-will-change-transform-001.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>CSS Test (Transforms): Transform and perspective with w negative</title>
+<link rel="author" title="L. David Baron" href="https://dbaron.org/">
+<link rel="author" title="Google" href="http://www.google.com/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=696374">
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+<link rel="match" href="rotate-animation-with-will-change-transform-001-ref.html">
+
+<!--
+
+This is a simplified version of one case within the Blink web test
+virtual/threaded-no-composited-antialiasing/animations/composited-animations-rotate-zero-degrees.html
+but with will-change: transform added, so that it fails with the bug
+that is introduced in the intermediate state of fixing
+https://bugs.chromium.org/p/chromium/issues/detail?id=696374
+
+-->
+
+<style>
+
+@keyframes a {
+ from { rotate: 0 1 0 44deg; }
+ to { rotate: 0 1 0 44deg; }
+}
+
+div {
+ width: 100px;
+ height: 100px;
+ animation: a linear 10s infinite;
+ /* rotate: 0 1 0 44deg; */
+ background: fuchsia;
+ transform-origin: 100px 0;
+ will-change: transform;
+}
+
+</style>
+
+<div></div>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-composition.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-composition.html
new file mode 100644
index 0000000000..18b5308ae8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-composition.html
@@ -0,0 +1,227 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title> rotate composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-rotate">
+<meta name="assert" content="rotate supports animation">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<body>
+<script>
+// Numerical precision may cause an axis aligned rotation to appear slightly
+// misaligned. Convert to (x, y, z, angle) form with rounding for comparison.
+function parseRotation(args) {
+ const array = args.split(' ');
+ if (array.length == 1) {
+ // Angle or 'none'.
+ return !!parseFloat(args) ? roundNumbers('0 0 1 ' + args) : args;
+ }
+ if (array.length == 2) {
+ // Axis name + angle
+ let axis = array[0];
+ let angle = array[1];
+ switch (array[0]) {
+ case 'x':
+ axis = '1 0 0 ';
+ break;
+ case 'y':
+ axis = '0 1 0';
+ break;
+ case 'z':
+ axis = '0 0 1';
+ break;
+ }
+ return roundNumbers(axis + ' ' + angle);
+ }
+ if (array.length == 4) {
+ // Axis as [x,y,z] triplet + angle.
+ // Normalize the axis (if possible) for comparison.
+ let x = parseFloat(array[0]);
+ let y = parseFloat(array[1]);
+ let z = parseFloat(array[2]);
+ const angle = array[3];
+ const length = Math.sqrt(x*x + y*y + z*z);
+ if (length > 1e-4) {
+ x /= length;
+ y /= length;
+ z /= length;
+ }
+ return roundNumbers(`${x} ${y} ${z} ${angle}`);
+ }
+ return args;
+}
+
+function compareRotations(actual, expected) {
+ assert_equals(parseRotation(actual), parseRotation(expected));
+}
+
+test_composition({
+ property: 'rotate',
+ underlying: '100deg',
+ addFrom: '10deg',
+ addTo: '30deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '90deg'},
+ {at: 0, expect: '110deg'},
+ {at: 0.25, expect: '115deg'},
+ {at: 0.75, expect: '125deg'},
+ {at: 1, expect: '130deg'},
+ {at: 2, expect: '150deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 0 0 200deg',
+ addFrom: '1 0 0 -100deg',
+ replaceTo: '1 0 0 40deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '1 0 0 160deg'},
+ {at: 0, expect: '1 0 0 100deg'},
+ {at: 0.25, expect: '1 0 0 85deg'},
+ {at: 0.75, expect: '1 0 0 55deg'},
+ {at: 1, expect: '1 0 0 40deg'},
+ {at: 2, expect: '1 0 0 -20deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '0 1 0 -40deg',
+ replaceFrom: '0 1 0 50deg',
+ addTo: '0 1 0 10deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0 1 0 130deg'},
+ {at: 0, expect: '0 1 0 50deg'},
+ {at: 0.25, expect: '0 1 0 30deg'},
+ {at: 0.75, expect: '0 1 0 -10deg'},
+ {at: 1, expect: '0 1 0 -30deg'},
+ {at: 2, expect: '0 1 0 -110deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 2 3 40deg',
+ addFrom: '2 4 6 10deg',
+ addTo: '3 6 9 50deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0.27 0.53 0.8 10deg'},
+ {at: 0, expect: '0.27 0.53 0.8 50deg'},
+ {at: 0.25, expect: '0.27 0.53 0.8 60deg'},
+ {at: 0.75, expect: '0.27 0.53 0.8 80deg'},
+ {at: 1, expect: '0.27 0.53 0.8 90deg'},
+ {at: 2, expect: '0.27 0.53 0.8 130deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 2 3 270deg',
+ addFrom: '1 2 3 90deg',
+ replaceTo: '0 1 0 100deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0 -1 0 100deg'},
+ {at: 0, expect: '0deg'},
+ {at: 0.25, expect: 'y 25deg'},
+ {at: 0.75, expect: 'y 75deg'},
+ {at: 1, expect: 'y 100deg'},
+ // Accept both the SLERP and the common axis solution, which are equivalent.
+ {at: 2, expect: '0 -1 0 160deg', option: 'y 200deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 2 3 90deg',
+ addFrom: '2 4 6 270deg',
+ replaceTo: '0 1 0 100deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0 -1 0 100deg'},
+ {at: 0, expect: '0deg'},
+ {at: 0.25, expect: 'y 25deg'},
+ {at: 0.75, expect: 'y 75deg'},
+ {at: 1, expect: 'y 100deg'},
+ // Accept both the SLERP and the common axis solution, which are equivalent.
+ {at: 2, expect: '0 -1 0 160deg', option: 'y 200deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 0 0 0deg',
+ addFrom: '1 1 0 90deg',
+ replaceTo: '0 1 1 135deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0.67 -0.06 -0.74 124.97deg'},
+ {at: 0, expect: '0.71 0.71 0 90deg'},
+ {at: 0.25, expect: '0.54 0.8 0.26 94.83deg'},
+ {at: 0.75, expect: '0.17 0.78 0.61 118.68deg'},
+ {at: 1, expect: '0 0.71 0.71 135deg'},
+ {at: 2, expect: '-0.52 0.29 0.81 208.96deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: 'none',
+ addFrom: 'none',
+ replaceTo: '0 1 0 100deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: 'y -100deg'},
+ {at: 0, expect: 'y 0deg'},
+ {at: 0.25, expect: 'y 25deg'},
+ {at: 0.75, expect: 'y 75deg'},
+ {at: 1, expect: 'y 100deg'},
+ {at: 2, expect: 'y 200deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: 'none',
+ addFrom: '2 4 6 270deg',
+ replaceTo: 'none',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0.27 0.53 0.8 540deg'},
+ {at: 0, expect: '0.27 0.53 0.8 270deg'},
+ {at: 0.25, expect: '0.27 0.53 0.8 202.5deg'},
+ {at: 0.75, expect: '0.27 0.53 0.8 67.5deg'},
+ {at: 1, expect: '0.27 0.53 0.8 0deg'},
+ {at: 2, expect: '0.27 0.53 0.8 -270deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 2 3 90deg',
+ addFrom: 'none',
+ replaceTo: '0 1 0 100deg',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0.31 -0.22 0.92 131.66deg'},
+ {at: 0, expect: '1 2 3 90deg'},
+ {at: 0.25, expect: '0.21 0.73 0.64 86.72deg'},
+ {at: 0.75, expect: '0.07 0.97 0.21 92.05deg'},
+ {at: 1, expect: '0 1 0 100deg'},
+ {at: 2, expect: '-0.2 0.79 -0.59 151.11deg'},
+]);
+
+test_composition({
+ property: 'rotate',
+ underlying: '1 2 3 90deg',
+ addFrom: '2 4 6 270deg',
+ replaceTo: 'none',
+ comparisonFunction: compareRotations
+}, [
+ {at: -1, expect: '0.27 0.53 0.8 720deg'},
+ {at: 0, expect: '0.27 0.53 0.8 360deg'},
+ {at: 0.25, expect: '0.27 0.53 0.8 270deg'},
+ {at: 0.75, expect: '0.27 0.53 0.8 90deg'},
+ {at: 1, expect: '0.27 0.53 0.8 0deg'},
+ {at: 2, expect: '0.27 0.53 0.8 -360deg'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-interpolation.html
new file mode 100644
index 0000000000..17fb7e499c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-interpolation.html
@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>rotate interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-rotate">
+<meta name="assert" content="rotate supports animation.">
+<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>
+</head>
+
+<style>
+.parent {
+ rotate: 90deg;
+}
+
+.target {
+ width: 40px;
+ height: 20px;
+ background-color: grey;
+ rotate: 10deg;
+}
+
+.expected {
+ background-color: green;
+}
+</style>
+
+<template id="target-template">
+<div class="parent">
+ <div class="target">Text</div>
+</div>
+</template>
+
+<body>
+<script>
+test_interpolation({
+ property: 'rotate',
+ from: '100deg',
+ to: '180deg',
+}, [
+ {at: -1, expect: '20deg'},
+ {at: 0, expect: '100deg'},
+ {at: 0.125, expect: '110deg'},
+ {at: 0.875, expect: '170deg'},
+ {at: 1, expect: '180deg'},
+ {at: 2, expect: '260deg'}
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '45deg',
+ to: '-1 1 0 60deg',
+}, [
+ {at: -1, expect: '0.447214 -0.447214 0.774597 104.478deg'},
+ {at: 0, expect: '45deg'},
+ {at: 0.125, expect: '-0.136456 0.136456 0.981203 40.6037deg'},
+ {at: 0.875, expect: '-0.70246 0.70246 0.114452 53.1994deg'},
+ {at: 1, expect: '-0.71 0.71 0 60deg'},
+ {at: 2, expect: '-0.637897 0.637897 -0.431479 124.975deg'}
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: 'none',
+ to: '7 -8 9 400grad',
+}, [
+ {at: -1, expect: '0.5 -0.57 0.65 -400grad'},
+ {at: 0, expect: '0.5 -0.57 0.65 0deg'},
+ {at: 0.125, expect: '0.5 -0.57 0.65 50grad'},
+ {at: 0.875, expect: '0.5 -0.57 0.65 350grad'},
+ {at: 1, expect: '0.5 -0.57 0.65 400grad'},
+ {at: 2, expect: '0.5 -0.57 0.65 800grad'}
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: 'none',
+ to: 'none',
+}, [
+ {at: -1, expect: 'none'},
+ {at: 0, expect: 'none'},
+ {at: 0.125, expect: 'none'},
+ {at: 0.875, expect: 'none'},
+ {at: 1, expect: 'none'},
+ {at: 2, expect: 'none'}
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: 'none',
+ to: '30deg',
+}, [
+ {at: -1, expect: '-30deg'},
+ {at: 0, expect: '0deg'},
+ {at: 0.25, expect: '7.5deg'},
+ {at: 0.75, expect: '22.5deg'},
+ {at: 1, expect: '30deg'},
+ {at: 2, expect: '60deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: neutralKeyframe,
+ to: '30deg',
+}, [
+ {at: -1, expect: '-10deg'},
+ {at: 0, expect: '10deg'},
+ {at: 0.25, expect: '15deg'},
+ {at: 0.75, expect: '25deg'},
+ {at: 1, expect: '30deg'},
+ {at: 2, expect: '50deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: 'inherit',
+ to: '270deg',
+}, [
+ {at: -1, expect: '-90deg'},
+ {at: 0, expect: '90deg'},
+ {at: 0.25, expect: '135deg'},
+ {at: 0.75, expect: '225deg'},
+ {at: 1, expect: '270deg'},
+ {at: 2, expect: '450deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: 'unset',
+ to: '30deg',
+}, [
+ {at: -1, expect: '-30deg'},
+ {at: 0, expect: '0deg'},
+ {at: 0.25, expect: '7.5deg'},
+ {at: 0.75, expect: '22.5deg'},
+ {at: 1, expect: '30deg'},
+ {at: 2, expect: '60deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '100deg',
+ to: '-100deg',
+}, [
+ {at: -1, expect: '300deg'},
+ {at: 0, expect: '100deg'},
+ {at: 0.25, expect: '50deg'},
+ {at: 0.75, expect: '-50deg'},
+ {at: 1, expect: '-100deg'},
+ {at: 2, expect: '-300deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '0 1 0 100deg',
+ to: '0 1 0 -100deg',
+}, [
+ {at: -1, expect: '0 1 0 300deg'},
+ {at: 0, expect: '0 1 0 100deg'},
+ {at: 0.25, expect: '0 1 0 50deg'},
+ {at: 0.75, expect: '0 1 0 -50deg'},
+ {at: 1, expect: '0 1 0 -100deg'},
+ {at: 2, expect: '0 1 0 -300deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '1 -2.5 3.64 100deg',
+ to: '1 -2.5 3.64 -100deg',
+}, [
+ {at: -1, expect: '0.22 -0.55 0.8 300deg'},
+ {at: 0, expect: '0.22 -0.55 0.8 100deg'},
+ {at: 0.25, expect: '0.22 -0.55 0.8 50deg'},
+ {at: 0.75, expect: '0.22 -0.55 0.8 -50deg'},
+ {at: 1, expect: '0.22 -0.55 0.8 -100deg'},
+ {at: 2, expect: '0.22 -0.55 0.8 -300deg'},
+]);
+
+// The rotation angle gets interpolated numerically and the rotation vector
+// of the non-zero angle is used or (0, 0, 1) if both angles are zero.
+// So, we have to convert "1 0 0 0deg" into "0 1 0 0deg", and apply the same
+// concept for other test cases.
+// https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
+test_interpolation({
+ property: 'rotate',
+ from: '1 0 0 0deg',
+ to: '0 1 0 10deg',
+}, [
+ {at: -1, expect: '0 1 0 -10deg'},
+ {at: 0, expect: '0 1 0 0deg'},
+ {at: 0.25, expect: '0 1 0 2.5deg'},
+ {at: 0.75, expect: '0 1 0 7.5deg'},
+ {at: 1, expect: '0 1 0 10deg'},
+ {at: 2, expect: '0 1 0 20deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '1 1 0 90deg',
+ to: '0 1 1 135deg',
+}, [
+ {at: -1, expect: '0.67 -0.06 -0.74 124.97deg'},
+ {at: 0, expect: '0.71 0.71 0 90deg'},
+ {at: 0.25, expect: '0.54 0.8 0.26 94.83deg'},
+ {at: 0.75, expect: '0.17 0.78 0.61 118.68deg'},
+ {at: 1, expect: '0 0.71 0.71 135deg'},
+ // The result in Blink is '0.52 -0.29 -0.81 151.04deg', and the result in
+ // Gecko is `-0.52 0.29 0.8 208.96deg`. Both of them can be represented as the
+ // same 3d rotation (but by an opposite direction vector and angle).
+ // The spec only mentions we should use Slerp to do interpolation for rotate
+ // property, but it seems the implementation detail for extrapolation are
+ // different (because this is not in the range of [0, 1]).
+ // For now, we make both results pass because their rendering results are the
+ // same.
+ {at: 2, expect: '0.52 -0.29 -0.81 151.04deg',
+ option: '-0.52 0.29 0.81 208.96deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '0 1 0 0deg',
+ to: '1 0 0 450deg',
+}, [
+ {at: -1, expect: '1 0 0 -450deg'},
+ {at: 0, expect: '1 0 0 0deg'},
+ {at: 0.25, expect: '1 0 0 112.5deg'},
+ {at: 0.75, expect: '1 0 0 337.5deg'},
+ {at: 1, expect: '1 0 0 450deg'},
+ {at: 2, expect: '1 0 0 900deg'},
+]);
+
+test_interpolation({
+ property: 'rotate',
+ from: '1 0 0 450deg',
+ to: '0 1 0 0deg',
+}, [
+ {at: -1, expect: '1 0 0 900deg'},
+ {at: 0, expect: '1 0 0 450deg'},
+ {at: 0.25, expect: '1 0 0 337.5deg'},
+ {at: 0.75, expect: '1 0 0 112.5deg'},
+ {at: 1, expect: '1 0 0 0deg'},
+ {at: 2, expect: '1 0 0 -450deg'},
+]);
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent-ref.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent-ref.html
new file mode 100644
index 0000000000..e95e62be88
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent-ref.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Reference for rotate transform equivalent</title>
+<script src="/common/reftest-wait.js"></script>
+<style>
+ .block {
+ border: 2px solid white; /* Avoid anti-aliasing artifacts */
+ height: 100px;
+ width: 100px;
+ position: absolute;
+ left: 100px;
+ top: 100px;
+ }
+
+ .overlay {
+ background: green;
+ z-index: 2;
+ }
+</style>
+<body>
+ <div id="transform" class="block overlay"></div>
+
+<script>
+ 'use strict';
+
+ async function waitForNextFrame() {
+ return new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ }
+
+ async function createAnimation(elementName, keyframes) {
+ const element = document.getElementById(elementName);
+ const anim = element.animate(keyframes, 1000);
+ anim.pause();
+ anim.currentTime = 500;
+ return anim.ready;
+ }
+
+ onload = async function() {
+ await waitForNextFrame();
+
+ await createAnimation('transform', [
+ {transform: 'matrix3d(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1)'},
+ {transform: 'matrix(0, 1, -1, 0, 0, 0)'}]);
+
+ await waitForNextFrame();
+ takeScreenshot();
+ };
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent.html b/testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent.html
new file mode 100644
index 0000000000..5748d86608
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/rotate-transform-equivalent.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Rotate transform equivalent</title>
+<link rel="match" href="rotate-transform-equivalent-ref.html">
+<meta name="fuzzy" content="maxDifference=0-46;totalPixels=0-277">
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#ctm">
+<script src="/common/reftest-wait.js"></script>
+<style>
+ .block {
+ border: 2px solid white; /* Avoid anti-aliasing artifacts */
+ height: 100px;
+ width: 100px;
+ position: absolute;
+ left: 100px;
+ top: 100px;
+ }
+
+ .rotation {
+ background: red;
+ }
+
+ .overlay {
+ background: green;
+ }
+
+ #rotateAdd {
+ rotate: 1 0 0 45deg;
+ }
+</style>
+<body>
+ <div id="rotateAdd" class="block rotation"></div>
+ <div id="rotateReplace" class="block rotation"></div>
+ <div id="transform" class="block overlay"></div>
+
+<script>
+ 'use strict';
+
+ async function waitForNextFrame() {
+ return new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ }
+
+ async function createAnimation(elementName, keyframes) {
+ const element = document.getElementById(elementName);
+ const anim = element.animate(keyframes, 1000);
+ anim.pause();
+ anim.currentTime = 500;
+ return anim.ready;
+ }
+
+ onload = async function() {
+ await waitForNextFrame();
+
+ await createAnimation('rotateAdd', [
+ {rotate: '1 0 0 45deg', composite: 'add'},
+ {rotate: '0 0 1 90deg'}]);
+ await createAnimation('rotateReplace', [
+ {rotate: '1 0 0 90deg'},
+ {rotate: '0 0 1 90deg'}]);
+
+ await createAnimation('transform', [
+ {transform: 'matrix3d(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1)'},
+ {transform: 'matrix(0, 1, -1, 0, 0, 0)'}]);
+
+ await waitForNextFrame();
+ takeScreenshot();
+ };
+</script>
+</body>
+
diff --git a/testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg-ref.html b/testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg-ref.html
new file mode 100644
index 0000000000..d555a5fbe5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Animating the "scale" property on an SVG element</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+
+<style>
+
+svg {
+ width: 400px;
+ height: 400px;
+}
+
+rect {
+ width: 100px;
+ height: 100px;
+ transform-origin: top left;
+ scale: 2;
+}
+
+</style>
+</head>
+<body>
+<svg><rect></rect></svg>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg.html b/testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg.html
new file mode 100644
index 0000000000..8862545688
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/scale-animation-on-svg.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<title>Animating the "scale" property on an SVG element</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+<link rel="match" href="scale-animation-on-svg-ref.html">
+
+<style>
+
+@keyframes scale-animation {
+ from { scale: 1; }
+ to { scale: 2; }
+}
+
+svg {
+ width: 400px;
+ height: 400px;
+}
+
+rect {
+ width: 100px;
+ height: 100px;
+ transform-origin: top left;
+ animation: scale-animation 1ms linear forwards;
+}
+
+</style>
+</head>
+<body>
+<svg><rect></rect></svg>
+
+<script>
+
+(async function() {
+ await Promise.all(document.getAnimations().map(animation => animation.finished));
+ document.documentElement.classList.remove("reftest-wait");
+})();
+
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-transforms/animation/scale-composition.html b/testing/web-platform/tests/css/css-transforms/animation/scale-composition.html
new file mode 100644
index 0000000000..107aa0b273
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/scale-composition.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>scale composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-scale">
+<meta name="assert" content="scale supports animation.">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<body>
+<script>
+test_composition({
+ property: 'scale',
+ underlying: '2 1',
+ addFrom: '3 1',
+ addTo: '4 1',
+}, [
+ {at: -0.5, expect: '5 1'},
+ {at: 0, expect: '6 1'},
+ {at: 0.25, expect: '6.5 1'},
+ {at: 0.5, expect: '7 1'},
+ {at: 0.75, expect: '7.5 1'},
+ {at: 1, expect: '8 1'},
+ {at: 1.5, expect: '9 1'},
+]);
+
+test_composition({
+ property: 'scale',
+ underlying: '1 2 3',
+ addFrom: '4 5 6',
+ replaceTo: '7 8 9',
+}, [
+ {at: -0.5, expect: '2.5 11 22.5'},
+ {at: 0, expect: '4 10 18'},
+ {at: 0.25, expect: '4.75 9.5 15.75'},
+ {at: 0.5, expect: '5.5 9 13.5'},
+ {at: 0.75, expect: '6.25 8.5 11.25'},
+ {at: 1, expect: '7 8 9'},
+ {at: 1.5, expect: '8.5 7 4.5'},
+]);
+
+test_composition({
+ property: 'scale',
+ underlying: 'none',
+ addFrom: 'none',
+ replaceTo: '1.5 1',
+}, [
+ {at: -1, expect: '0.5 1'},
+ {at: 0, expect: '1'},
+ {at: 0.25, expect: '1.125 1'},
+ {at: 0.75, expect: '1.375 1'},
+ {at: 1, expect: '1.5 1'},
+ {at: 2, expect: '2 1'},
+]);
+
+test_composition({
+ property: 'scale',
+ underlying: 'none',
+ addFrom: '4 5 6',
+ replaceTo: 'none',
+}, [
+ {at: -1, expect: '7 9 11'},
+ {at: 0, expect: '4 5 6'},
+ {at: 0.25, expect: '3.25 4 4.75'},
+ {at: 0.75, expect: '1.75 2 2.25'},
+ {at: 1, expect: '1'},
+ {at: 2, expect: '-2 -3 -4'},
+]);
+
+test_composition({
+ property: 'scale',
+ underlying: '1 2 3',
+ addFrom: 'none',
+ replaceTo: '7 8 9',
+}, [
+ {at: -0.5, expect: '-2 -1 0'},
+ {at: 0, expect: '1 2 3'},
+ {at: 0.25, expect: '2.5 3.5 4.5'},
+ {at: 0.5, expect: '4 5 6'},
+ {at: 0.75, expect: '5.5 6.5 7.5'},
+ {at: 1, expect: '7 8 9'},
+ {at: 1.5, expect: '10 11 12'},
+]);
+
+test_composition({
+ property: 'scale',
+ underlying: '1 2 3',
+ addFrom: '4 5 6',
+ replaceTo: 'none',
+}, [
+ {at: -0.5, expect: '5.5 14.5 26.5'},
+ {at: 0, expect: '4 10 18'},
+ {at: 0.25, expect: '3.25 7.75 13.75'},
+ {at: 0.5, expect: '2.5 5.5 9.5'},
+ {at: 0.75, expect: '1.75 3.25 5.25'},
+ {at: 1, expect: '1'},
+ {at: 1.5, expect: '-0.5 -3.5 -7.5'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/scale-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/scale-interpolation.html
new file mode 100644
index 0000000000..0e33371955
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/scale-interpolation.html
@@ -0,0 +1,242 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>scale interpolation</title>
+ <link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-scale">
+ <meta name="assert" content="scale supports animation.">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/css/support/interpolation-testcommon.js"></script>
+ <style>
+ .parent {
+ scale: 0.5 1 2
+ }
+ .target {
+ width: 10px;
+ height: 10px;
+ background-color: black;
+ scale: 1.1 1;
+ }
+ .expected {
+ background-color: green;
+ }
+ </style>
+ </head>
+ <body>
+ <template id="target-template">
+ <div class="parent">
+ <div class="target"></div>
+ </div>
+ </template>
+
+ <script>
+ // Matching two <number> version.
+ test_interpolation({
+ property: 'scale',
+ from: '-10 5',
+ to: '10 -5',
+ }, [
+ {at: -1, expect: '-30 15'},
+ {at: 0, expect: '-10 5'},
+ {at: 0.25, expect: '-5 2.5'},
+ {at: 0.75, expect: '5 -2.5'},
+ {at: 1, expect: '10 -5'},
+ {at: 2, expect: '30 -15'},
+ ]);
+
+ // Matching three <number> version.
+ test_interpolation({
+ property: 'scale',
+ from: '2 30 400',
+ to: '10 110 1200',
+ }, [
+ {at: -1, expect: '-6 -50 -400'},
+ {at: 0, expect: '2 30 400'},
+ {at: 0.125, expect: '3 40 500'},
+ {at: 0.875, expect: '9 100 1100'},
+ {at: 1, expect: '10 110 1200'},
+ {at: 2, expect: '18 190 2000'}
+ ]);
+
+ // From three <number> to two <number>; test that it expands correctly.
+ test_interpolation({
+ property: 'scale',
+ from: '26 17 9',
+ to: '2 1',
+ }, [
+ {at: -1, expect: '50 33 17'},
+ {at: 0, expect: '26 17 9'},
+ {at: 0.125, expect: '23 15 8'},
+ {at: 0.875, expect: '5 3 2'},
+ {at: 1, expect: '2 1'},
+ {at: 2, expect: '-22 -15 -7'}
+ ]);
+
+ // Test one <number> is expanded correctly.
+ test_interpolation({
+ property: 'scale',
+ from: '1',
+ to: '10 -5 0',
+ }, [
+ {at: -1, expect: '-8 7 2'},
+ {at: 0, expect: '1'},
+ {at: 0.25, expect: '3.25 -0.5 0.75'},
+ {at: 0.75, expect: '7.75 -3.5 0.25'},
+ {at: 1, expect: '10 -5 0'},
+ {at: 2, expect: '19 -11 -1'},
+ ]);
+
+ test_interpolation({
+ property: 'scale',
+ from: '-10 5 1',
+ to: '1',
+ }, [
+ {at: -1, expect: '-21 9'},
+ {at: 0, expect: '-10 5'},
+ {at: 0.25, expect: '-7.25 4'},
+ {at: 0.75, expect: '-1.75 2'},
+ {at: 1, expect: '1'},
+ {at: 2, expect: '12 -3'},
+ ]);
+
+ // Handling of the none value.
+ test_interpolation({
+ property: 'scale',
+ from: 'none',
+ to: 'none',
+ }, [
+ {at: -1, expect: 'none'},
+ {at: 0, expect: 'none'},
+ {at: 0.125, expect: 'none'},
+ {at: 0.875, expect: 'none'},
+ {at: 1, expect: 'none'},
+ {at: 2, expect: 'none'}
+ ]);
+
+ // Going from none to a valid value; test that it converts properly.
+ test_interpolation({
+ property: 'scale',
+ from: 'none',
+ to: '4 3 2',
+ }, [
+ {at: -1, expect: '-2 -1 0'},
+ {at: 0, expect: '1'},
+ {at: 0.125, expect: '1.375 1.25 1.125'},
+ {at: 0.875, expect: '3.625 2.75 1.875'},
+ {at: 1, expect: '4 3 2'},
+ {at: 2, expect: '7 5 3'}
+ ]);
+
+ // Test neutral keyframe; make sure it adds the underlying.
+ test_interpolation({
+ property: 'scale',
+ from: neutralKeyframe,
+ to: '1.5 1',
+ }, [
+ {at: -1, expect: '0.7 1'},
+ {at: 0, expect: '1.1 1'},
+ {at: 0.25, expect: '1.2 1'},
+ {at: 0.75, expect: '1.4 1'},
+ {at: 1, expect: '1.5 1'},
+ {at: 2, expect: '1.9 1'},
+ ]);
+
+ // Test initial value; for 'scale' this is 'none'.
+ test_interpolation({
+ property: 'scale',
+ from: 'initial',
+ to: '2 0.5 1',
+ }, [
+ {at: -1, expect: '0 1.5'},
+ {at: 0, expect: '1'},
+ {at: 0.25, expect: '1.25 0.875'},
+ {at: 0.75, expect: '1.75 0.625'},
+ {at: 1, expect: '2 0.5'},
+ {at: 2, expect: '3 0'},
+ ]);
+
+ test_interpolation({
+ property: 'scale',
+ from: '2 0.5 1',
+ to: 'initial',
+ }, [
+ {at: -1, expect: '3 0'},
+ {at: 0, expect: '2 0.5'},
+ {at: 0.25, expect: '1.75 0.6251'},
+ {at: 0.75, expect: '1.25 0.875'},
+ {at: 1, expect: '1'},
+ {at: 2, expect: '0 1.5'},
+ ]);
+
+
+ // Test unset value; for 'scale' this is 'none'.
+ test_interpolation({
+ property: 'scale',
+ from: 'unset',
+ to: '1.5 1',
+ }, [
+ {at: -1, expect: '0.5 1'},
+ {at: 0, expect: '1'},
+ {at: 0.25, expect: '1.125 1'},
+ {at: 0.75, expect: '1.375 1'},
+ {at: 1, expect: '1.5 1'},
+ {at: 2, expect: '2 1'},
+ ]);
+
+ // Test inherited value.
+ test_interpolation({
+ property: 'scale',
+ from: 'inherit',
+ to: '2 0.5 1',
+ }, [
+ {at: -1, expect: '-1 1.5 3'},
+ {at: 0, expect: '0.5 1 2'},
+ {at: 0.25, expect: '0.875 0.875 1.75'},
+ {at: 0.75, expect: '1.625 0.625 1.25'},
+ {at: 1, expect: '2 0.5'},
+ {at: 2, expect: '3.5 0 0'},
+ ]);
+
+ test_interpolation({
+ property: 'scale',
+ from: '2 0.5 1',
+ to: 'inherit',
+ }, [
+ {at: -1, expect: '3.5 0 0'},
+ {at: 0, expect: '2 0.5'},
+ {at: 0.25, expect: '1.625 0.625 1.25'},
+ {at: 0.75, expect: '0.875 0.875 1.75'},
+ {at: 1, expect: '0.5 1 2'},
+ {at: 2, expect: '-1 1.5 3'},
+ ]);
+
+ // Test combination of initial and inherit.
+ test_interpolation({
+ property: 'scale',
+ from: 'initial',
+ to: 'inherit',
+ }, [
+ {at: -1, expect: '1.5 1 0'},
+ {at: 0, expect: '1'},
+ {at: 0.25, expect: '0.875 1 1.25'},
+ {at: 0.75, expect: '0.625 1 1.75'},
+ {at: 1, expect: '0.5 1 2'},
+ {at: 2, expect: '0 1 3'},
+ ]);
+
+ test_interpolation({
+ property: 'scale',
+ from: 'inherit',
+ to: 'initial',
+ }, [
+ {at: -1, expect: '0 1 3'},
+ {at: 0, expect: '0.5 1 2'},
+ {at: 0.25, expect: '0.625 1 1.75'},
+ {at: 0.75, expect: '0.875 1 1.25'},
+ {at: 1, expect: '1'},
+ {at: 2, expect: '1.5 1 0'},
+ ]);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/support/transform-interpolation-reftests.js b/testing/web-platform/tests/css/css-transforms/animation/support/transform-interpolation-reftests.js
new file mode 100644
index 0000000000..d79cbedefe
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/support/transform-interpolation-reftests.js
@@ -0,0 +1,193 @@
+'use strict';
+
+// Each test is an array of [endpoint, midpoint, endpoint] and tests
+// whether the endpoints interpolate to the same visual state as the midpoint
+const transformTests = {
+ translate: [
+ ['translateX(0px)', 'translateX(25px)', 'translateX(50px)'],
+ ['translateY(0px)', 'translateY(25px)', 'translateY(50px)'],
+ ['translateX(0%)', 'translateX(25%)', 'translateX(50%)'],
+ ['translateY(0%)', 'translateY(25%)', 'translateY(50%)'],
+ ['translateX(50%)', 'translate(25%, 25%)', 'translateY(50%)'],
+ ['translateX(50%)', 'translate(25%, 25px)', 'translateY(50px)'],
+ ['translateX(50px)', 'translateX(calc(25px + 25%))', 'translateX(50%)']
+ ],
+ translateEm: [
+ ['translateX(0em)', 'translateX(2em)', 'translateX(4em)'],
+ ['translateX(-50px)', 'translateX(calc(2em - 25px))', 'translateX(4em)'],
+ ['translateX(50%)', 'translateX(calc(25% - 2em))', 'translateX(-4em)']
+ ],
+ rotate: [
+ // Rotation about named-axis.
+ ['rotate(30deg)', 'rotate(60deg)', 'rotate(90deg)'],
+ ['rotateX(30deg)', 'rotateX(60deg)', 'rotateX(90deg)'],
+ ['rotateY(30deg)', 'rotateY(60deg)', 'rotateY(90deg)'],
+ ['rotate(30deg)', 'rotate(60deg)', 'rotateZ(90deg)'],
+ ['rotate(0deg)', 'rotate(180deg)', 'rotate(360deg)'],
+ // Common axis rotations.
+ ['rotate3d(7, 8, 9, 0deg)', 'rotate3d(7, 8, 9, 45deg)', 'rotate3d(7, 8, 9, 90deg)'],
+ ['rotate3d(1, 2, 3, 0deg)', 'rotate3d(3, 6, 9, 45deg)', 'rotate3d(2, 4, 6, 90deg)'],
+ // Axis is arbitrary if angle is zero. Use non-zero rotation to determine
+ // the rotation axis.
+ ['rotateX(0deg)', 'rotate(45deg)', 'rotate(90deg)'],
+ ['rotateX(90deg)', 'rotateX(45deg)', 'rotate(0deg)']
+ ],
+ rotateSlerp: [
+ // First endpoint is the same rotation as rotateZ(0deg) but triggers SLERP
+ ['rotateX(360deg)', 'rotateZ(45deg)', 'rotateZ(90deg)'],
+ // Interpolation with inverse. Second case is a common-axis case, but
+ // included here to group it with its equivalent SLERP test.
+ ['rotate(45deg)', 'rotate(0deg)', 'rotate3d(0, 0, -1, 45deg)'],
+ ['rotate(45deg)', 'rotate(0deg)', 'rotate(-45deg)'],
+ // Interpolate axis and angle of rotation.
+ // 70.5288deg = acos(1/3).
+ ['rotateX(90deg)', 'rotate3d(1, 1, 0, 70.5288deg)', 'rotateY(90deg)'],
+ // Not nice analytical solution for this last one.
+ // (1, 1, 0, 90deg) --> (x, y, z, w) = (1/2, 1/2, 0, 1/root2)
+ // (0, 1, 1, 180deg) --> (x, y, z, w) = (0, 1/root2, 1/root2, 0)
+ // Trace of the "to" transformation matrix is -1. Requires special handling
+ // to ensure correctness of the quaternion.
+ // SLERP @0.5: (x, y, z, w) = (0.30389062997686395,
+ // 0.7336568918027127,
+ // 0.4297662618258487,
+ // 0.4297662618258487)
+ // --> rotate3d(0.3365568, 0.8125199, 0.4759632, 129.094547486deg)
+ ['rotate3d(1, 1, 0, 90deg)',
+ 'rotate3d(0.3365568, 0.8125199, 0.4759632, 129.094547486deg)',
+ 'rotate3d(0, 1, 1, 180deg)'],
+ ],
+ scale: [
+ ['scaleX(0.5)', 'scaleX(0.75)', 'scaleX(1)'],
+ ['scaleY(0.5)', 'scaleY(0.75)', 'scaleY(1)'],
+ ['scale(0.5)', 'scale(0.75)', 'scale(1)'],
+ ['scaleX(0.5)', 'scale(0.75)', 'scaleY(0.5)'],
+ ['scale3d(0.5, 1, 2)', 'scale3d(0.75, 0.75, 3)', 'scale3d(1, 0.5, 4)']
+ ],
+ skew: [
+ ['skewX(0deg)', 'skewX(30deg)', 'skewX(60deg)'],
+ ['skewY(0deg)', 'skewY(30deg)', 'skewY(60deg)'],
+ ['skew(60deg, 0deg)', 'skew(30deg, 30deg)', 'skew(0deg, 60deg)'],
+ ['skewX(0deg) rotate(0deg)', 'skewX(0deg) rotate(180deg)', 'skewX(0deg) rotate(360deg)'],
+ ['skewX(0deg) rotate(0deg)', 'matrix(1, 0, 0, 1, 0, 0)', 'skewY(0deg) rotate(360deg)']
+ ],
+ matrix: [
+ // matched matrix parameters do not collapse the values after them
+ ['matrix(1,0,0,1,0,0) rotate(0deg)', 'matrix(1.5,0,0,1.5,0,0) rotate(180deg)', 'matrix(2,0,0,2,0,0) rotate(360deg)']
+ ],
+ perspective: [
+ // Since perspective doesn't do anything on its own, we need to
+ // combine it with a transform that does.
+ ['perspective(none) translateZ(15px)', 'perspective(none) translateZ(15px)', 'perspective(none) translateZ(15px)'],
+ ['perspective(100px) translateZ(50px)', 'perspective(200px) translateZ(50px)', 'perspective(none) translateZ(50px)'],
+ ['perspective(none) translateZ(15px)', 'perspective(50px) translateZ(15px)', 'perspective(25px) translateZ(15px)'],
+ ['perspective(100px) translateZ(15px)', 'perspective(40px) translateZ(15px)', 'perspective(25px) translateZ(15px)'],
+
+ // Test that perspective is clamped to 1px.
+ ['perspective(0.1px) translateZ(0.25px)', 'perspective(1px) translateZ(0.25px)', 'perspective(0.1px) translateZ(0.25px)'],
+ ['perspective(0px) translateZ(0.25px)', 'perspective(1px) translateZ(0.25px)', 'perspective(0px) translateZ(0.25px)'],
+ ['perspective(0px) translateZ(0.5px)', 'perspective(1.5px) translateZ(0.5px)', 'perspective(3px) translateZ(0.5px)'],
+ { test: ['perspective(10px) translateZ(0.5px)', 'translateZ(0.5px)', 'perspective(1px) translateZ(0.5px)'], midpoint: -1 },
+ { test: ['perspective(1px) translateZ(0.5px)', 'perspective(1px) translateZ(0.5px)', 'perspective(10px) translateZ(0.5px)'], midpoint: -1 }
+ ]
+};
+
+// Initial setup, which includes properties that will be overridden to
+// test invalidation.
+function initialStyle(div) {
+ div.style.width = '180px';
+ div.style.height = '150px';
+ div.style.margin = '50px';
+ div.style.borderLeft = 'solid 40px blue';
+ div.style.backgroundColor = 'green';
+ div.style.willChange = 'transform';
+ div.style.fontSize = '30px';
+}
+
+function finalStyle(div) {
+ div.style.width = '80px';
+ div.style.height = '80px';
+ div.style.fontSize = '15px';
+}
+
+function styleBody(){
+ let body = document.body;
+ body.style.display = 'flex';
+ body.style.flexDirection = 'row';
+ body.style.flexWrap = 'wrap';
+}
+
+// Simulate a static image at 50% progress with a running animation.
+// The easing curve has zero slope and curvature at its midpoint of 50% -> 50%.
+// The timing values are chosen so as so that a delay of up to 10s will not
+// cause a visual change.
+const duration = 1e9;
+const midpointOptions = {
+ easing: 'cubic-bezier(0,1,1,0)',
+ duration: duration,
+ delay: -duration/2
+};
+
+// Similar to midpointOptions, but to produce the interpolation result
+// at -1 instead of the interpolation result at 0.5. This easing curve
+// has zero slope at its midpoint of -100% (though does have curvature).
+const negoneOptions = {
+ easing: 'cubic-bezier(0,-1,1,-2)',
+ duration: duration,
+ delay: -duration/2
+};
+
+// Indices to unpack a test case, which is in the format
+// [start, midpoint, end]
+const startIndex = 0;
+const midIndex = 1;
+const endIndex = 2;
+
+async function createTests(tests) {
+ styleBody();
+ for (const obj of tests) {
+ let test = ("test" in obj) ? obj.test : obj;
+ let midpoint = ("midpoint" in obj) ? obj.midpoint : 0.5;
+ let options;
+ if (midpoint == 0.5) {
+ options = midpointOptions;
+ } else if (midpoint == -1) {
+ options = negoneOptions;
+ } else {
+ document.appendChild(document.createTextNode("unexpected midpoint " + midpoint));
+ }
+ let div = document.createElement('div');
+ document.body.appendChild(div);
+ initialStyle(div);
+ var anim =
+ div.animate({transform: [test[startIndex], test[endIndex]]}, options);
+ await anim.ready;
+ finalStyle(div); // Change size to test invalidation.
+ }
+
+ await new Promise(requestAnimationFrame);
+ await new Promise(requestAnimationFrame);
+ takeScreenshot();
+}
+
+// Create references using an animation with identical keyframes for start
+// and end so as to avoid rounding and anti-aliasing differences between
+// animated and non-animated pathways.
+async function createRefs(tests) {
+ styleBody();
+ for (const obj of tests) {
+ let test = ("test" in obj) ? obj.test : obj;
+ let div = document.createElement('div');
+ document.body.appendChild(div);
+ initialStyle(div);
+ finalStyle(div);
+ var anim = div.animate(
+ {transform: [test[midIndex], test[midIndex]]},
+ midpointOptions);
+ await anim.ready;
+ }
+
+ await new Promise(requestAnimationFrame);
+ await new Promise(requestAnimationFrame);
+ takeScreenshot();
+}
+
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-composition.html
new file mode 100644
index 0000000000..928da71a9b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-composition.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#transform-property">
+<meta name="assert" content="transform 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>
+// This file contains tests for the composition behavior of transforms that is
+// unrelated to the individual transform functions. For the transform functions
+// themselves, see the transform-*-composition.html subtests.
+
+// ------------------ Addition -----------------
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(100deg) rotateY(100deg)',
+ addFrom: 'translate(10px, 20px)',
+ replaceTo: 'rotateX(200deg) rotateY(200deg) translate(110px, 220px)',
+}, [
+ {at: -0.5, expect: 'rotateX(50deg) rotateY(50deg) translate(-40px, -80px)'},
+ {at: 0, expect: 'rotateX(100deg) rotateY(100deg) translate(10px, 20px)'},
+ {at: 0.25, expect: 'rotateX(125deg) rotateY(125deg) translate(35px, 70px)'},
+ {at: 0.5, expect: 'rotateX(150deg) rotateY(150deg) translate(60px, 120px)'},
+ {at: 0.75, expect: 'rotateX(175deg) rotateY(175deg) translate(85px, 170px)'},
+ {at: 1, expect: 'rotateX(200deg) rotateY(200deg) translate(110px, 220px)'},
+ {at: 1.5, expect: 'rotateX(250deg) rotateY(250deg) translate(160px, 320px)'},
+]);
+
+// Shorter list is extended with corresponding identity transforms for pairwise
+// interpolation.
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(45deg)',
+ addFrom: 'none',
+ addTo: 'rotateY(360deg)',
+}, [
+ {at: -0.5, expect: 'rotateX(45deg) rotateY(-180deg)'},
+ {at: 0, expect: 'rotateX(45deg) rotateY(0deg)'},
+ {at: 0.25, expect: 'rotateX(45deg) rotateY(90deg)'},
+ {at: 0.5, expect: 'rotateX(45deg) rotateY(180deg)'},
+ {at: 0.75, expect: 'rotateX(45deg) rotateY(270deg)'},
+ {at: 1, expect: 'rotateX(45deg) rotateY(360deg)'},
+ {at: 1.5, expect: 'rotateX(45deg) rotateY(540deg)'},
+]);
+
+// Matrix decomposition cases
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(90deg)',
+ addFrom: 'translate(100px, 100px)',
+ addTo: 'scale(2)',
+}, [
+ {at: -0.5, expect: 'matrix3d(0.5, 0, 0, 0, 0, 1.11022e-16, 0.5, 0, 0, -1, 2.22045e-16, 0, 150, 9.18485e-15, 150, 1)'},
+ {at: 0, expect: 'matrix3d(1, 0, 0, 0, 0, 6.12323e-17, 1, 0, 0, -1, 6.12323e-17, 0, 100, 6.12323e-15, 100, 1)'},
+ {at: 0.25, expect: 'matrix3d(1.25, 0, 0, 0, 0, 2.77556e-16, 1.25, 0, 0, -1, 2.22045e-16, 0, 75, 4.59243e-15, 75, 1)'},
+ {at: 0.5, expect: 'matrix3d(1.5, 0, 0, 0, 0, 3.33067e-16, 1.5, 0, 0, -1, 2.22045e-16, 0, 50, 3.06162e-15, 50, 1)'},
+ {at: 0.75, expect: 'matrix3d(1.75, 0, 0, 0, 0, 3.88578e-16, 1.75, 0, 0, -1, 2.22045e-16, 0, 25, 1.53081e-15, 25, 1)'},
+ {at: 1, expect: 'matrix3d(2, 0, 0, 0, 0, 1.22465e-16, 2, 0, 0, -1, 6.12323e-17, 0, 0, 0, 0, 1)'},
+ {at: 1.5, expect: 'matrix3d(2.5, 0, 0, 0, 0, 5.55112e-16, 2.5, 0, 0, -1, 2.22045e-16, 0, -50, -3.06162e-15, -50, 1)'},
+]);
+
+// Force a fallback to matrix interpolation.
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(45deg)',
+ addFrom: 'scaleX(1)',
+ addTo: 'rotateY(360deg)',
+}, [
+ {at: -0.5, expect: 'rotateX(45deg)'},
+ {at: 0, expect: 'rotateX(45deg)'},
+ {at: 0.25, expect: 'rotateX(45deg)'},
+ {at: 0.5, expect: 'rotateX(45deg)'},
+ {at: 0.75, expect: 'rotateX(45deg)'},
+ {at: 1, expect: 'rotateX(45deg)'},
+ {at: 1.5, expect: 'rotateX(45deg)'},
+]);
+
+// ------------------ Accumulation -----------------
+
+// TODO(smcgruer): Add tests for accumulation behaviors.
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-001.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-001.html
new file mode 100644
index 0000000000..94bb87c6e7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-001.html
@@ -0,0 +1,325 @@
+<!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>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-002.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-002.html
new file mode 100644
index 0000000000..fec0de1f53
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-002.html
@@ -0,0 +1,151 @@
+<!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">
+
+<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>
+// Scale
+test_interpolation({
+ property: 'transform',
+ from: 'scale(10, 5)',
+ to: 'scale(20, 9)'
+}, [
+ {at: -1, expect: 'scale(0, 1)'},
+ {at: 0, expect: 'scale(10, 5)'},
+ {at: 0.25, expect: 'scale(12.5, 6)'},
+ {at: 0.75, expect: 'scale(17.5, 8)'},
+ {at: 1, expect: 'scale(20, 9)'},
+ {at: 2, expect: 'scale(30, 13)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scaleX(10)',
+ to: 'scaleX(20)'
+}, [
+ {at: -1, expect: 'scaleX(0)'},
+ {at: 0, expect: 'scaleX(10)'},
+ {at: 0.25, expect: 'scaleX(12.5)'},
+ {at: 0.75, expect: 'scaleX(17.5)'},
+ {at: 1, expect: 'scaleX(20)'},
+ {at: 2, expect: 'scaleX(30)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scaleY(5)',
+ to: 'scaleY(9)'
+}, [
+ {at: -1, expect: 'scaleY(1)'},
+ {at: 0, expect: 'scaleY(5)'},
+ {at: 0.25, expect: 'scaleY(6)'},
+ {at: 0.75, expect: 'scaleY(8)'},
+ {at: 1, expect: 'scaleY(9)'},
+ {at: 2, expect: 'scaleY(13)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scaleZ(1)',
+ to: 'scaleZ(2)'
+}, [
+ {at: -1, expect: 'scaleZ(0)'},
+ {at: 0, expect: 'scaleZ(1)'},
+ {at: 0.25, expect: 'scaleZ(1.25)'},
+ {at: 0.75, expect: 'scaleZ(1.75)'},
+ {at: 1, expect: 'scaleZ(2)'},
+ {at: 2, expect: 'scaleZ(3)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scale3d(10, 0.5, 1)',
+ to: 'scale3d(20, 1, 2)'
+}, [
+ {at: -1, expect: 'scale3d(0, 0, 0)'},
+ {at: 0, expect: 'scale3d(10, 0.5, 1)'},
+ {at: 0.25, expect: 'scale3d(12.5, 0.625, 1.25)'},
+ {at: 0.75, expect: 'scale3d(17.5, 0.875, 1.75)'},
+ {at: 1, expect: 'scale3d(20, 1, 2)'},
+ {at: 2, expect: 'scale3d(30, 1.5, 3)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scaleX(0)',
+ to: 'scaleY(0)',
+}, [
+ {at: -1, expect: 'scale(-1, 2)'},
+ {at: 0, expect: 'scale(0, 1)'},
+ {at: 0.25, expect: 'scale(0.25, 0.75)'},
+ {at: 0.75, expect: 'scale(0.75, 0.25)'},
+ {at: 1, expect: 'scale(1, 0)'},
+ {at: 2, expect: 'scale(2, -1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'none',
+ to: 'scale3d(2, 3, 5)'
+}, [
+ {at: -1, expect: 'scale3d(0, -1, -3)'},
+ {at: 0, expect: 'scale3d(1, 1, 1)'},
+ {at: 0.25, expect: 'scale3d(1.25, 1.5, 2)'},
+ {at: 0.75, expect: 'scale3d(1.75, 2.5, 4)'},
+ {at: 1, expect: 'scale3d(2, 3, 5)'},
+ {at: 2, expect: 'scale3d(3, 5, 9)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scale3d(2, 3, 5)',
+ to: 'none'
+}, [
+ {at: -1, expect: 'scale3d(3, 5, 9)'},
+ {at: 0, expect: 'scale3d(2, 3, 5)'},
+ {at: 0.25, expect: 'scale3d(1.75, 2.5, 4)'},
+ {at: 0.75, expect: 'scale3d(1.25, 1.5, 2)'},
+ {at: 1, expect: 'scale3d(1, 1, 1)'},
+ {at: 2, expect: 'scale3d(0, -1, -3)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scaleX(10) scaleY(0.5) scaleZ(1)',
+ to: 'scaleX(20) scaleY(1) scaleZ(2)'
+}, [
+ {at: -1, expect: 'scaleX(0) scaleY(0) scaleZ(0)'},
+ {at: 0, expect: 'scaleX(10) scaleY(0.5) scaleZ(1)'},
+ {at: 0.25, expect: 'scaleX(12.5) scaleY(0.625) scaleZ(1.25)'},
+ {at: 0.75, expect: 'scaleX(17.5) scaleY(0.875) scaleZ(1.75)'},
+ {at: 1, expect: 'scaleX(20) scaleY(1) scaleZ(2)'},
+ {at: 2, expect: 'scaleX(30) scaleY(1.5) scaleZ(3)'},
+]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-003.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-003.html
new file mode 100644
index 0000000000..4386bdbfa4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-003.html
@@ -0,0 +1,128 @@
+<!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">
+
+<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>
+test_interpolation({
+ property: 'transform',
+ from: 'skewX(10rad) scaleZ(1)',
+ to: 'skewX(20rad) scaleZ(2)'
+}, [
+ {at: -1, expect: 'skewX(0rad) scaleZ(0)'},
+ {at: 0, expect: 'skewX(10rad) scaleZ(1)'},
+ {at: 0.25, expect: 'skewX(12.5rad) scaleZ(1.25)'},
+ {at: 0.75, expect: 'skewX(17.5rad) scaleZ(1.75)'},
+ {at: 1, expect: 'skewX(20rad) scaleZ(2)'},
+ {at: 2, expect: 'skewX(30rad) scaleZ(3)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'skewX(10rad)',
+ to: 'skewX(20rad) scaleZ(2)'
+}, [
+ {at: -1, expect: 'skewX(0rad) scaleZ(0)'},
+ {at: 0, expect: 'skewX(10rad) scaleZ(1)'},
+ {at: 0.25, expect: 'skewX(12.5rad) scaleZ(1.25)'},
+ {at: 0.75, expect: 'skewX(17.5rad) scaleZ(1.75)'},
+ {at: 1, expect: 'skewX(20rad) scaleZ(2)'},
+ {at: 2, expect: 'skewX(30rad) scaleZ(3)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'scaleZ(3) perspective(400px)',
+ to: 'scaleZ(4) skewX(1rad) perspective(500px)'
+}, [
+ {at: -1, expect: 'scaleZ(2) matrix3d(1, 0, 0, 0, -1.55741, 1, 0, 0, 0, 0, 1, -0.003, 0, 0, 0, 1)'},
+ {at: 0, expect: 'scaleZ(3) matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -0.0025, 0, 0, 0, 1)'},
+ {at: 0.25, expect: 'scaleZ(3.25) matrix3d(1, 0, 0, 0, 0.389352, 1, 0, 0, 0, 0, 1, -0.002375, 0, 0, 0, 1)'},
+ {at: 0.75, expect: 'scaleZ(3.75) matrix3d(1, 0, 0, 0, 1.16806, 1, 0, 0, 0, 0, 1, -0.002125, 0, 0, 0, 1)'},
+ {at: 1, expect: 'scaleZ(4) matrix3d(1, 0, 0, 0, 1.55741, 1, 0, 0, 0, 0, 1, -0.002, 0, 0, 0, 1)'},
+ {at: 2, expect: 'scaleZ(5) matrix3d(1, 0, 0, 0, 3.11482, 1, 0, 0, 0, 0, 1, -0.0015, 0, 0, 0, 1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translateY(70%) scaleZ(1)',
+ to: 'translateY(90%) scaleZ(2)'
+}, [
+ {at: -1, expect: 'translateY(50%) scaleZ(0)'},
+ {at: 0, expect: 'translateY(70%) scaleZ(1)'},
+ {at: 0.25, expect: 'translateY(75%) scaleZ(1.25)'},
+ {at: 0.75, expect: 'translateY(85%) scaleZ(1.75)'},
+ {at: 1, expect: 'translateY(90%) scaleZ(2)'},
+ {at: 2, expect: 'translateY(110%) scaleZ(3)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translateY(70%)',
+ to: 'translateY(90%) scaleZ(2)'
+}, [
+ {at: -1, expect: 'translateY(50%) scaleZ(0)'},
+ {at: 0, expect: 'translateY(70%)'},
+ {at: 0.25, expect: 'translateY(75%) scaleZ(1.25)'},
+ {at: 0.75, expect: 'translateY(85%) scaleZ(1.75)'},
+ {at: 1, expect: 'translateY(90%) scaleZ(2)'},
+ {at: 2, expect: 'translateY(110%) scaleZ(3)'},
+]);
+
+// Skew
+test_interpolation({
+ property: 'transform',
+ from: 'skewX(10rad)',
+ to: 'skewX(20rad)'
+}, [
+ {at: -1, expect: 'skewX(0rad)'},
+ {at: 0, expect: 'skewX(10rad)'},
+ {at: 0.25, expect: 'skewX(12.5rad)'},
+ {at: 0.75, expect: 'skewX(17.5rad)'},
+ {at: 1, expect: 'skewX(20rad)'},
+ {at: 2, expect: 'skewX(30rad)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'skewY(10rad)',
+ to: 'skewY(20rad)'
+}, [
+ {at: -1, expect: 'skewY(0rad)'},
+ {at: 0, expect: 'skewY(10rad)'},
+ {at: 0.25, expect: 'skewY(12.5rad)'},
+ {at: 0.75, expect: 'skewY(17.5rad)'},
+ {at: 1, expect: 'skewY(20rad)'},
+ {at: 2, expect: 'skewY(30rad)'},
+]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-004.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-004.html
new file mode 100644
index 0000000000..a6b3c75c62
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-004.html
@@ -0,0 +1,187 @@
+<!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">
+
+<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>
+// Translate
+test_interpolation({
+ property: 'transform',
+ from: 'translate(12px, 70%)',
+ to: 'translate(13px, 90%)'
+}, [
+ {at: -1, expect: 'translate(11px, 50%)'},
+ {at: 0, expect: 'translate(12px, 70%)'},
+ {at: 0.25, expect: 'translate(12.25px, 75%)'},
+ {at: 0.75, expect: 'translate(12.75px, 85%)'},
+ {at: 1, expect: 'translate(13px, 90%)'},
+ {at: 2, expect: 'translate(14px, 110%)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translateX(12px)',
+ to: 'translateX(13px)'
+}, [
+ {at: -1, expect: 'translateX(11px)'},
+ {at: 0, expect: 'translateX(12px)'},
+ {at: 0.25, expect: 'translateX(12.25px)'},
+ {at: 0.75, expect: 'translateX(12.75px)'},
+ {at: 1, expect: 'translateX(13px)'},
+ {at: 2, expect: 'translateX(14px)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translateY(70%)',
+ to: 'translateY(90%)'
+}, [
+ {at: -1, expect: 'translateY(50%)'},
+ {at: 0, expect: 'translateY(70%)'},
+ {at: 0.25, expect: 'translateY(75%)'},
+ {at: 0.75, expect: 'translateY(85%)'},
+ {at: 1, expect: 'translateY(90%)'},
+ {at: 2, expect: 'translateY(110%)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translateZ(2em)',
+ to: 'translateZ(3em)'
+}, [
+ {at: -1, expect: 'translateZ(1em)'},
+ {at: 0, expect: 'translateZ(2em)'},
+ {at: 0.25, expect: 'translateZ(2.25em)'},
+ {at: 0.75, expect: 'translateZ(2.75em)'},
+ {at: 1, expect: 'translateZ(3em)'},
+ {at: 2, expect: 'translateZ(4em)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translate3d(12px, 70%, 2em)',
+ to: 'translate3d(13px, 90%, 3em)'
+}, [
+ {at: -1, expect: 'translate3d(11px, 50%, 1em)'},
+ {at: 0, expect: 'translate3d(12px, 70%, 2em)'},
+ {at: 0.25, expect: 'translate3d(12.25px, 75%, 2.25em)'},
+ {at: 0.75, expect: 'translate3d(12.75px, 85%, 2.75em)'},
+ {at: 1, expect: 'translate3d(13px, 90%, 3em)'},
+ {at: 2, expect: 'translate3d(14px, 110%, 4em)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translateX(12px) translateY(70%) translateZ(2em)',
+ to: 'translateX(13px) translateY(90%) translateZ(3em)'
+}, [
+ {at: -1, expect: 'translateX(11px) translateY(50%) translateZ(1em)'},
+ {at: 0, expect: 'translateX(12px) translateY(70%) translateZ(2em)'},
+ {at: 0.25, expect: 'translateX(12.25px) translateY(75%) translateZ(2.25em)'},
+ {at: 0.75, expect: 'translateX(12.75px) translateY(85%) translateZ(2.75em)'},
+ {at: 1, expect: 'translateX(13px) translateY(90%) translateZ(3em)'},
+ {at: 2, expect: 'translateX(14px) translateY(110%) translateZ(4em)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'skewX(10rad) translateY(70%)',
+ to: 'skewX(20rad) translateY(90%)'
+}, [
+ {at: -1, expect: 'skewX(0rad) translateY(50%)'},
+ {at: 0, expect: 'skewX(10rad) translateY(70%)'},
+ {at: 0.25, expect: 'skewX(12.5rad) translateY(75%)'},
+ {at: 0.75, expect: 'skewX(17.5rad) translateY(85%)'},
+ {at: 1, expect: 'skewX(20rad) translateY(90%)'},
+ {at: 2, expect: 'skewX(30rad) translateY(110%)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'skewX(1rad)',
+ to: 'translate3d(8px, -4px, 12px) skewX(2rad)'
+}, [
+ {at: -1, expect: 'matrix3d(1, 0, 0, 0, 5.2998553125713235, 1, 0, 0, 0, 0, 1, 0, -8, 4, -12, 1)'},
+ {at: 0, expect: 'matrix(1, 0, 1.5574077246549023, 1, 0, 0)'},
+ {at: 0.25, expect: 'matrix3d(1, 0, 0, 0, 0.621795827675797, 1, 0, 0, 0, 0, 1, 0, 2, -1, 3, 1)'},
+ {at: 0.75, expect: 'matrix3d(1, 0, 0, 0, -1.2494279662824135, 1, 0, 0, 0, 0, 1, 0, 6, -3, 9, 1)'},
+ {at: 1, expect: 'matrix3d(1, 0, 0, 0, -2.185039863261519, 1, 0, 0, 0, 0, 1, 0, 8, -4, 12, 1)'},
+ {at: 2, expect: 'matrix3d(1, 0, 0, 0, -5.9274874511779405, 1, 0, 0, 0, 0, 1, 0, 16, -8, 24, 1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translate3d(8px, -4px, 12px) skewX(1rad) perspective(400px)',
+ to: 'scaleY(2) skewX(2rad) perspective(500px)'
+}, [
+ {at: -1, expect: 'matrix3d(1, 0, 0, 0, 0, 0, 0, 0, -0.03876288659793814, 0.01938144329896907, 0.94, -0.0029653608247422686, 16, -8, 24, 0.986144329896907)'},
+ {at: 0, expect: 'matrix3d(1, 0, 0, 0, 1.5574077246549023, 1, 0, 0, -0.02, 0.01, 0.97, -0.0025, 8, -4, 12, 1)'},
+ {at: 0.25, expect: 'matrix3d(1, 0, 0, 0, 1.1186572632293585, 1.25, 0, 0, -0.0151159793814433, 0.00755798969072165, 0.9775, -0.002378247422680413, 6, -3, 9, 1.0012989690721648)'},
+ {at: 0.75, expect: 'matrix3d(1, 0, 0, 0, -0.7525665307288518, 1.75, 0, 0, -0.005115979381443298, 0.002557989690721649, 0.9924999999999999, -0.002128247422680412, 2, -1, 3, 1.001298969072165)'},
+ {at: 1, expect: 'matrix3d(1, 0, 0, 0, -2.185039863261519, 2, 0, 0, 0, 0, 1, -0.002, 0, 0, 0, 1)'},
+ {at: 2, expect: 'matrix3d(1, 0, 0, 0, -11.227342763749263, 3, 0, 0, 0.021237113402061854, -0.010618556701030927, 1.03, -0.0014653608247422677, -8, 4, -12, 0.9861443298969074)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translate3d(8px, -4px, 12px) skewX(1rad) perspective(400px)',
+ to: 'translate3d(4px, -12px, 8px) scaleY(2) perspective(500px)'
+}, [
+ {at: -1, expect: 'translate3d(12px, 4px, 16px) matrix3d(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -0.003, 0, 0, 0, 1)'},
+ {at: 0, expect: 'translate3d(8px, -4px, 12px) matrix3d(1, 0, 0, 0, 1.55741, 1, 0, 0, 0, 0, 1, -0.0025, 0, 0, 0, 1)'},
+ {at: 0.25, expect: 'translate3d(7px, -6px, 11px) matrix3d(1, 0, 0, 0, 1.46007, 1.25, 0, 0, 0, 0, 1, -0.002375, 0, 0, 0, 1)'},
+ {at: 0.75, expect: 'translate3d(5px, -10px, 9px) matrix3d(1, 0, 0, 0, 0.681366, 1.75, 0, 0, 0, 0, 1, -0.002125, 0, 0, 0, 1)'},
+ {at: 1, expect: 'translate3d(4px, -12px, 8px) matrix3d(1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, -0.002, 0, 0, 0, 1)'},
+ {at: 2, expect: 'translate3d(0px, -20px, 4px) matrix3d(1, 0, 0, 0, -4.67222, 3, 0, 0, 0, 0, 1, -0.0015, 0, 0, 0, 1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translate3d(8px, -4px, 12px) skewX(1rad) perspective(400px)',
+ to: 'translate3d(4px, -12px, 8px) skewX(2rad) scaleY(2)'
+}, [
+ {at: -1, expect: 'translate3d(12px, 4px, 16px) skewX(0rad) matrix3d(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -0.005, 0, 0, 0, 1)'},
+ {at: 0, expect: 'translate3d(8px, -4px, 12px) skewX(1rad) matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -0.0025, 0, 0, 0, 1)'},
+ {at: 0.25, expect: 'translate3d(7px, -6px, 11px) skewX(1.25rad) matrix3d(1, 0, 0, 0, 0, 1.25, 0, 0, 0, 0, 1, -0.001875, 0, 0, 0, 1)'},
+ {at: 0.75, expect: 'translate3d(5px, -10px, 9px) skewX(1.75rad) matrix3d(1, 0, 0, 0, 0, 1.75, 0, 0, 0, 0, 1, -0.000625, 0, 0, 0, 1)'},
+ {at: 1, expect: 'translate3d(4px, -12px, 8px) skewX(2rad) matrix(1, 0, 0, 2, 0, 0)'},
+ {at: 2, expect: 'translate3d(0px, -20px, 4px) skewX(3rad) matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0.0025, 0, 0, 0, 1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'translate3D(100px, 200px, 300px)',
+ to: 'none'
+}, [
+ {at: -1, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 200, 400, 600, 1)'},
+ {at: 0, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 100, 200, 300, 1)'},
+ {at: 0.25, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 75, 150, 225, 1)'},
+ {at: 0.75, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 25, 50, 75, 1)'},
+ {at: 1, expect: 'matrix(1, 0, 0, 1, 0, 0) '},
+ {at: 2, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -100, -200, -300, 1)'},
+]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-005.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-005.html
new file mode 100644
index 0000000000..879ff3f435
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-005.html
@@ -0,0 +1,268 @@
+<!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>
+// Matrix transforms:
+
+// 2D matrix transforms:
+//
+// [m11 m21 0 m41] [1 0 0 Tx] [cos(R) -sin(R) 0 0] [1 K 0 0] [Sx 0 0 0]
+// [m12 m22 0 m42] = [0 1 0 Ty] [sin(R) cos(R) 0 0] [0 1 0 0] [0 Sy 0 0]
+// [ 0 0 1 0 ] [0 0 1 0 ] [ 0 0 1 0] [0 0 1 0] [0 0 1 0]
+// [ 0 0 0 1 ] [0 0 0 1 ] [ 0 0 0 1] [0 0 0 1] [0 0 0 1]
+//
+// M = translate * rotate * skew * scale
+// See also webkit-transform-interpolation-005.html
+//
+
+const cos30 = Math.cos(Math.PI / 6);
+const sin30 = Math.sin(Math.PI / 6);
+const cos45 = Math.cos(Math.PI / 4);
+const sin45 = Math.sin(Math.PI / 4);
+const cos60 = Math.cos(Math.PI / 3);
+const sin60 = Math.sin(Math.PI / 3);
+
+// translateY(-6px) -> translateX(6px) rotate(90deg) scaleX(7)
+test_interpolation({
+ property: 'transform',
+ from: 'matrix(1, 0, 0, 1, 0, -6)',
+ to: 'matrix(0, 7, -1, 0, 6, 0)'
+}, [
+ {at: -1, expect: 'matrix(0, 5, 1, 0, -6, -12)'},
+ {at: 0, expect: 'matrix(1, 0, 0, 1, 0, -6)'},
+ {
+ at: 1/3,
+ expect: `matrix(${3 * cos30}, ${3 * sin30}, -${sin30}, ${cos30}, 2, -4)`
+ },
+ {
+ at: 0.5,
+ expect: `matrix(${4 * cos45}, ${4 * sin45}, -${sin45}, ${cos45}, 3, -3)`
+ },
+ {
+ at: 2/3,
+ expect: `matrix(${5 * cos60}, ${5 * sin60}, -${sin60}, ${cos60}, 4, -2)`
+ },
+ {at: 1, expect: 'matrix(0, 7, -1, 0, 6, 0)'},
+ {at: 2, expect: 'matrix(-13, 0, 0, -1, 12, 6)'}
+]);
+
+// translateX(6px) rotate(90deg) scaleX(7) -> translateY(-6px)
+test_interpolation({
+ property: 'transform',
+ from: 'matrix(0, 7, -1, 0, 6, 0)',
+ to: 'matrix(1, 0, 0, 1, 0, -6)',
+}, [
+ {at: -1, expect: 'matrix(-13, 0, 0, -1, 12, 6)'},
+ {at: 0, expect: 'matrix(0, 7, -1, 0, 6, 0)'},
+ {
+ at: 1/3,
+ expect: `matrix(${5 * cos60}, ${5 * sin60}, -${sin60}, ${cos60}, 4, -2)`
+ },
+ {
+ at: 0.5,
+ expect: `matrix(${4 * cos45}, ${4 * sin45}, -${sin45}, ${cos45}, 3, -3)`
+ },
+ {
+ at: 2/3,
+ expect: `matrix(${3 * cos30}, ${3 * sin30}, -${sin30}, ${cos30}, 2, -4)`
+ },
+ {at: 1, expect: 'matrix(1, 0, 0, 1, 0, -6)'},
+ {at: 2, expect: 'matrix(0, 5, 1, 0, -6, -12)'}
+]);
+
+// scaleY(7) -> skewX(45deg) scaleX(7)
+test_interpolation({
+ property: 'transform',
+ from: 'matrix(1, 0, 0, 7, 0, 0)',
+ to: 'matrix(7, 0, 1, 1, 0, 0)'
+}, [
+ {at: -1, expect: 'matrix(-5, 0, -13, 13, 0, 0)'},
+ {at: 0, expect: 'matrix(1, 0, 0, 7, 0, 0)'},
+ {at: 1/3, expect: 'matrix(3, 0, 1.6667, 5, 0, 0)'},
+ {at: 0.5, expect: 'matrix(4, 0, 2, 4, 0, 0)'},
+ {at: 2/3, expect: 'matrix(5, 0, 2, 3, 0, 0)'},
+ {at: 1, expect: `matrix(7, 0, 1, 1, 0, 0)`},
+ {at: 2, expect: `matrix(13, 0, -10, -5, 0, 0)`}
+]);
+
+// none -> translateX(6px) skewX(45deg) scaleX(7) scaleY(2)
+test_interpolation({
+ property: 'transform',
+ from: 'none',
+ to: 'matrix(7, 0, 2, 2, 6, 0)'
+}, [
+ {at: -1, expect: 'matrix(-5, 0, 0, 0, -6, 0)'},
+ {at: 0, expect: 'matrix(1, 0, 0, 1, 0, 0)'},
+ {at: 0.25, expect: 'matrix(2.5, 0, 0.31, 1.25, 1.5, 0)'},
+ {at: 0.5, expect: 'matrix(4, 0, 0.75, 1.5, 3, 0)'},
+ {at: 0.75, expect: 'matrix(5.5, 0, 1.31, 1.75, 4.5, 0)'},
+ {at: 1, expect: 'matrix(7, 0, 2, 2, 6, 0)'},
+ {at: 2, expect: 'matrix(13, 0, 6, 3, 12, 0)'}
+]);
+
+// translateY(-6px) scale(3, 5) -> none
+test_interpolation({
+ property: 'transform',
+ from: 'matrix(3, 0, 0, 5, 0, -6)',
+ to: 'none'
+}, [
+ {at: -1, expect: 'matrix(5, 0, 0, 9, 0, -12)'},
+ {at: 0, expect: 'matrix(3, 0, 0, 5, 0, -6)'},
+ {at: 0.25, expect: 'matrix(2.5, 0, 0, 4, 0, -4.5)'},
+ {at: 0.5, expect: 'matrix(2, 0, 0, 3, 0, -3)'},
+ {at: 0.75, expect: 'matrix(1.5, 0, 0, 2, 0, -1.5)'},
+ {at: 1, expect: 'matrix(1, 0, 0, 1, 0, 0)'},
+ {at: 2, expect: 'matrix(-1, 0, 0, -3, 0, 6)'}
+]);
+
+// 3-D matrix transforms.
+// TODO(kevers): Revisit 3D transform examples. It is difficult to infer
+// the quality of the matrix decompositions from the expected output.
+test_interpolation({
+ property: 'transform',
+ from: 'none',
+ to: 'matrix3d(1.0806046117362795, 0, -1.682941969615793, 0, 0, 3, 0, 0, 3.365883939231586, 0, 2.161209223472559, 0, 0, 0, 0, 1)'
+}, [
+ {at: -1, expect: 'matrix3d(0, 0, 0, 0, 0, -1, 0, 0, 1.682941969615793, 0, -1.0806046117362795, 0, 0, 0, 0, 1)'},
+ {at: 0, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'},
+ {at: 0.25, expect: 'matrix3d(1.211140527138306, 0, -0.30925494906815365, 0, 0, 1.5, 0, 0, 0.43295692869541513, 0, 1.6955967379936283, 0, 0, 0, 0, 1)'},
+ {at: 0.75, expect: 'matrix3d(1.2804555205291865, 0, -1.1928678300408346, 0, 0, 2.5, 0, 0, 2.215325970075836, 0, 2.377988823839918, 0, 0, 0, 0, 1)'},
+ {at: 1, expect: 'matrix3d(1.0806046117362795, 0, -1.682941969615793, 0, 0, 3, 0, 0, 3.365883939231586, 0, 2.161209223472559, 0, 0, 0, 0, 1)'},
+ {at: 2, expect: 'matrix3d(-1.2484405096414273, 0, -2.727892280477045, 0, 0, 5, 0, 0, 6.365081987779772, 0, -2.9130278558299967, 0, 0, 0, 0, 1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'matrix3d(1.0806046117362795, 0, -1.682941969615793, 0, 0, 3, 0, 0, 3.365883939231586, 0, 2.161209223472559, 0, 0, 0, 0, 1)',
+ to: 'none'
+}, [
+ {at: -1, expect: 'matrix3d(-1.2484405096414273, 0, -2.727892280477045, 0, 0, 5, 0, 0, 6.365081987779772, 0, -2.9130278558299967, 0, 0, 0, 0, 1)'},
+ {at: 0, expect: 'matrix3d(1.0806046117362795, 0, -1.682941969615793, 0, 0, 3, 0, 0, 3.365883939231586, 0, 2.161209223472559, 0, 0, 0, 0, 1)'},
+ {at: 0.25, expect: 'matrix3d(1.2804555205291865, 0, -1.1928678300408346, 0, 0, 2.5, 0, 0, 2.215325970075836, 0, 2.377988823839918, 0, 0, 0, 0, 1)'},
+ {at: 0.75, expect: 'matrix3d(1.211140527138306, 0, -0.30925494906815365, 0, 0, 1.5, 0, 0, 0.43295692869541513, 0, 1.6955967379936283, 0, 0, 0, 0, 1)'},
+ {at: 1, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'},
+ {at: 2, expect: 'matrix3d(0, 0, 0, 0, 0, -1, 0, 0, 1.682941969615793, 0, -1.0806046117362795, 0, 0, 0, 0, 1)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'matrix3d(2.3505561943, 0.0, 0.0, 0.0, 0.0, 2.6068943664, 0.0, 0.0, 0.0, 0.0, 2.6591082592, 0.0, 20.3339914256, 20.6709033765, 20.9147808456, 1.0)',
+ to: 'matrix3d(2.7133590938, 0.0, 0.0, 0.0, 0.0, 2.4645137761, 0.0, 0.0, 0.0, 0.0, 2.801687476, 0.0, 20.4335882254, 20.2330661998, 20.4583968206, 1.0)'
+}, [
+ {at: -1, expect: 'matrix3d(1.9877532948000005, 0.0, 0.0, 0.0, 0.0, 2.7492749567000003, 0.0, 0.0, 0.0, 0.0, 2.5165290423999997, 0.0, 20.2343946258, 21.1087405532, 21.371164870599998, 1.0)'},
+ {at: 0, expect: 'matrix3d(2.3505561943, 0.0, 0.0, 0.0, 0.0, 2.6068943664, 0.0, 0.0, 0.0, 0.0, 2.6591082592, 0.0, 20.3339914256, 20.6709033765, 20.9147808456, 1.0)'},
+ {at: 0.25, expect: 'matrix3d(2.441256919175, 0.0, 0.0, 0.0, 0.0, 2.571299218825, 0.0, 0.0, 0.0, 0.0, 2.6947530634, 0.0, 20.35889062555, 20.561444082325, 20.800684839349998, 1.0)'},
+ {at: 0.75, expect: 'matrix3d(2.622658368925, 0.0, 0.0, 0.0, 0.0, 2.500108923675, 0.0, 0.0, 0.0, 0.0, 2.7660426718, 0.0, 20.408689025450002, 20.342525493975, 20.572492826850002, 1.0)'},
+ {at: 1, expect: 'matrix3d(2.7133590938, 0.0, 0.0, 0.0, 0.0, 2.4645137761, 0.0, 0.0, 0.0, 0.0, 2.801687476, 0.0, 20.4335882254, 20.2330661998, 20.4583968206, 1.0)'},
+ {at: 2, expect: 'matrix3d(3.0761619932999995, 0.0, 0.0, 0.0, 0.0, 2.3221331858, 0.0, 0.0, 0.0, 0.0, 2.9442666928000003, 0.0, 20.5331850252, 19.7952290231, 20.002012795600002, 1.0)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'none',
+ to: 'matrix3d(0, 0.6875, -0.625, 0.3125, -0.6666666666666665, -1, 0.8333333333333334, 0.125, -0.6666666666666665, 0, 0.5, 1.0625, -1.1875, -0.0625, 1.3125, 1)'
+}, [
+ {at: -1, expect: 'matrix3d(-0.0000000000000002377810622383943, -1.0671050586638147, -0.08972656766237302, 1.3740432449326199, 0.98484601036295, -2.653201092395309, 0.6753819540610847, 3.6127240080250744, -2.7988839807429846, -1.2090004194153336, -0.5183744226115445, -0.7936088631686278, 1.1875, 0.0625, -1.3125, 5.340768914473683)'},
+ {at: 0, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'},
+ {at: 0.25, expect: 'matrix3d(0.9041890962319094, 0.3522701519297133, -0.15240204298176957, -0.1428256720529315, -0.7579798772527586, 0.6803606288839232, -0.05133336076757235, 0.37904689530895724, -0.1957679784745485, 0.38554138029509327, 0.8226186974340638, 0.3370288143441876, -0.296875, -0.015625, 0.328125, 0.5930529142680923)'},
+ {at: 0.75, expect: 'matrix3d(0.35007413226026135, 0.7254385504141292, -0.4977009150941454, 0.09582061929004702, -1.1027525038949482, -0.5884810398827429, 0.4516829688651701, 0.5447944343861767, -0.68717798815684, 0.2657772247405681, 0.5465690479810023, 1.0836207863885503, -0.890625, -0.046875, 0.984375, 0.5930529142680927)'},
+ {at: 1, expect: 'matrix3d(0, 0.6875, -0.625, 0.3125, -0.6666666666666665, -1, 0.8333333333333334, 0.125, -0.6666666666666665, 0, 0.5, 1.0625, -1.1875, -0.0625, 1.3125, 1)'},
+ {at: 2, expect: 'matrix3d(-0.5844534449366048, -0.42278005999296053, -0.4650580659922564, -0.6817595809063256, 0.9156938760088464, 0.3851647027225889, 0.9244443507516923, 0.7218225020358241, -0.0803568793574344, 0.1719974850210706, -0.49676609633513097, -0.25968177786904373, -2.375, -0.125, 2.625, 5.340768914473685)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'matrix3d(0, 0.6875, -0.625, 0.3125, -0.6666666666666665, -1, 0.8333333333333334, 0.125, -0.6666666666666665, 0, 0.5, 1.0625, -1.1875, -0.0625, 1.3125, 1)',
+ to: 'matrix3d(0.571428571428571, -0.625, -0.8333333333333346, -0.66666666666669, 0.5, -0.1875, -0.8125, 0.3125, 0.34375, -1, 0.8333333333333327, 1.34375, -1.34375, 1, -0.9375, 1)'
+}, [
+ {at: -1, expect: 'matrix3d(-0.6299594065765657, -0.10825090106268696, -0.20133311671001855, 5.485724217214554, 6.358051978686152, 0.16496896269344588, 1.5760051143537075, -54.21568355620423, 0.7106057459805782, -1.1596356050622005, -0.11495342545397585, -4.913752963990824, -1.03125, -1.125, 3.5625, -5.901513951904114)'},
+ {at: 0, expect: 'matrix3d(0, 0.6875, -0.625, 0.3125, -0.6666666666666665, -1, 0.8333333333333334, 0.125, -0.6666666666666665, 0, 0.5, 1.0625, -1.1875, -0.0625, 1.3125, 1)'},
+ {at: 0.25, expect: 'matrix3d(0.33652832679595723, 0.55254445148386, -0.7544724447833296, 0.22700224951774267, -0.69720168363685, -0.036373245768780864, 0.28149188169180933, -0.2845156818045006, -0.24737156018941048, 0.31207160370190334, 0.4564821058052897, 0.9220853089096839, -1.2265625, 0.203125, 0.75, 1.647016932991011)'},
+ {at: 0.75, expect: 'matrix3d(0.6861191524977764, -0.18025672746204927, -0.8710297237546482, 0.6072134247444672, 0.2819931018922366, 0.27778974607679663, -0.6540128246146626, 0.5063632314069845, 0.5509562084361049, -0.3215202993119732, 0.5459062603735321, 2.8697154005492105, -1.3046875, 0.734375, -0.375, 1.6470169329910096)'},
+ {at: 1, expect: 'matrix3d(0.571428571428571, -0.625, -0.8333333333333346, -0.66666666666669, 0.5, -0.1875, -0.8125, 0.3125, 0.34375, -1, 0.8333333333333327, 1.34375, -1.34375, 1, -0.9375, 1)'},
+ {at: 2, expect: 'matrix3d(-1.1789992641434441, -0.7109729379601547, -0.4455746537954199, -21.703089533128907, -0.11137581475421703, -0.08822983871000473, -0.05695380894007451, -2.22667264132605, -3.1443917136741506, 1.8952588096345078, 2.426615889772007, -21.697523130750138, -1.5, 2.0625, -3.1875, -5.901513951904121)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'matrix3d(0.571428571428571, -0.625, -0.8333333333333346, -0.66666666666669, 0.5, -0.1875, -0.8125, 0.3125, 0.34375, -1, 0.8333333333333327, 1.34375, -1.34375, 1, -0.9375, 1)',
+ to: 'none'
+}, [
+ {at: -1, expect: 'matrix3d(-0.6413028394192518, -1.0702420910513302, -0.5807595966791961, -18.02447171345163, 0.8211815704840004, 1.0980679097347057, 0.9399408862655454, 22.460730852026064, 0.28421009261178104, -0.5408346238741739, 0.5194791363698213, 3.075163035391172, -2.6875, 2, -1.875, -14.881239394516232)'},
+ {at: 0, expect: 'matrix3d(0.571428571428571, -0.625, -0.8333333333333346, -0.66666666666669, 0.5, -0.1875, -0.8125, 0.3125, 0.34375, -1, 0.8333333333333327, 1.34375, -1.34375, 1, -0.9375, 1)'},
+ {at: 0.25, expect: 'matrix3d(0.7912976716694541, -0.4517927901159618, -0.6868745974719376, 1.2522201536338506, 0.7952183069582651, 0.06340410955800829, -0.7956629784232128, 2.2561737435012983, 0.345639443327071, -0.8934490945546473, 0.830131443385676, 1.2606901484983566, -1.0078125, 0.75, -0.703125, 2.4888661932358946)'},
+ {at: 0.75, expect: 'matrix3d(1.0093457700315165, -0.12746048375025829, -0.24746788943106088, 1.3202120308857304, 0.6128364656690982, 0.7600694601651116, -0.22233359857303325, 1.4081483224940277, 0.21669805381113447, -0.3786082265932788, 0.908354523914928, 0.6747509193960347, -0.3359375, 0.25, -0.234375, 2.4888661932358964)'},
+ {at: 1, expect: 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'},
+ {at: 2, expect: 'matrix3d(0.39048513570444376, 0.14780794797065988, 0.6963068100217401, -4.857907861239344, -2.967682789284791, 0.6004978769584385, -3.5472376016872444, 26.675324787979896, -2.5953724498995308, 1.6280843851961373, 0.8163834310586356, 9.001735256585825, 1.34375, -1, 0.9375, -14.881239394516227)'},
+]);
+
+// Mismatched interpolation with an empty list should not use decomposition.
+test_interpolation({
+ property: 'transform',
+ from: 'none',
+ to: 'rotate(180deg)'
+}, [
+ {at: -1, expect: 'rotate(-180deg)'},
+ {at: 0, expect: 'rotate(0deg)'},
+ {at: 0.25, expect: 'rotate(45deg)'},
+ {at: 0.75, expect: 'rotate(135deg)'},
+ {at: 1, expect: 'rotate(180deg)'},
+ {at: 2, expect: 'rotate(360deg)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'rotate(180deg)',
+ to: 'none'
+}, [
+ {at: -1, expect: 'rotate(360deg)'},
+ {at: 0, expect: 'rotate(180deg)'},
+ {at: 0.25, expect: 'rotate(135deg)'},
+ {at: 0.75, expect: 'rotate(45deg)'},
+ {at: 1, expect: 'rotate(0deg)'},
+ {at: 2, expect: 'rotate(-180deg)'},
+]);
+test_interpolation({
+ property: 'transform',
+ from: 'none',
+ to: 'rotate(360deg)'
+}, [
+ {at: -1, expect: 'rotate(-360deg)'},
+ {at: 0, expect: 'rotate(0deg)'},
+ {at: 0.25, expect: 'rotate(90deg)'},
+ {at: 0.75, expect: 'rotate(270deg)'},
+ {at: 1, expect: 'rotate(360deg)'},
+ {at: 2, expect: 'rotate(720deg)'},
+]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-006.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-006.html
new file mode 100644
index 0000000000..c69bad7c93
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-006.html
@@ -0,0 +1,100 @@
+<!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">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<style>
+.parent {
+ transform: translate(30px);
+}
+.target {
+ color: white;
+ width: 100px;
+ height: 100px;
+ background-color: black;
+ display: inline-block;
+ overflow: hidden;
+ transform: translate(10px);
+}
+.expected {
+ background-color: green;
+}
+.parent {
+ transform: 30px;
+}
+.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>
+test_interpolation({
+ property: 'transform',
+ from: neutralKeyframe,
+ to: 'translate(20px)',
+}, [
+ {at: -1, expect: 'translate(0px)'},
+ {at: 0, expect: 'translate(10px)'},
+ {at: 0.25, expect: 'translate(12.5px)'},
+ {at: 0.75, expect: 'translate(17.5px)'},
+ {at: 1, expect: 'translate(20px)'},
+ {at: 2, expect: 'translate(30px)'},
+]);
+
+test_interpolation({
+ property: 'transform',
+ from: 'initial',
+ to: 'translate(20px)',
+}, [
+ {at: -1, expect: 'translate(-20px)'},
+ {at: 0, expect: 'translate(0px)'},
+ {at: 0.25, expect: 'translate(5px)'},
+ {at: 0.75, expect: 'translate(15px)'},
+ {at: 1, expect: 'translate(20px)'},
+ {at: 2, expect: 'translate(40px)'},
+]);
+
+test_interpolation({
+ property: 'transform',
+ from: 'inherit',
+ to: 'translate(20px)',
+}, [
+ {at: -1, expect: 'translate(40px)'},
+ {at: 0, expect: 'translate(30px)'},
+ {at: 0.25, expect: 'translate(27.5px)'},
+ {at: 0.75, expect: 'translate(22.5px)'},
+ {at: 1, expect: 'translate(20px)'},
+ {at: 2, expect: 'translate(10px)'},
+]);
+
+test_interpolation({
+ property: 'transform',
+ from: 'unset',
+ to: 'translate(20px)',
+}, [
+ {at: -1, expect: 'translate(-20px)'},
+ {at: 0, expect: 'translate(0px)'},
+ {at: 0.25, expect: 'translate(5px)'},
+ {at: 0.75, expect: 'translate(15px)'},
+ {at: 1, expect: 'translate(20px)'},
+ {at: 2, expect: 'translate(40px)'},
+]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-animated-ref.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-animated-ref.html
new file mode 100644
index 0000000000..0930a7469a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-animated-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+const testName = window.location.search.substr(1);
+createRefsWithAnimation(transformTests[testName]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-computed-value.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-computed-value.html
new file mode 100644
index 0000000000..4a836f3ca1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-computed-value.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform interpolation</title>
+<link rel="help" href="https://drafts.css-houdini.org/css-typed-om/#transformvalue-objects">
+<meta name="assert" content="transform gives the correct computed values when interpolated">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+
+<body>
+<script>
+function interpolation_test(from, to, expected_50) {
+ test(t => {
+ let div = createDiv(t);
+ let anim = div.animate({transform: [from, to]}, 2000);
+ anim.pause();
+ anim.currentTime = 1000;
+ let halfway = div.computedStyleMap().get('transform').toString();
+ assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
+ }, "Interpolation between " + from + " and " + to + " gives the correct " +
+ "computed value halfway according to computedStyleMap.");
+
+ test(t => {
+ let div = createDiv(t);
+ div.style.zoom = 1.25;
+ let anim = div.animate({transform: [from, to]}, 2000);
+ anim.pause();
+ anim.currentTime = 1000;
+ let halfway = div.computedStyleMap().get('transform').toString();
+ assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
+ }, "Interpolation between " + from + " and " + to + " gives the correct " +
+ "computed value halfway according to computedStyleMap with zoom active.");
+}
+
+interpolation_test('translateX(0px)', 'translateX(50px)', 'translate(25px, 0px)');
+interpolation_test('translateX(0%)', 'translateX(50%)', 'translate(25%, 0px)');
+interpolation_test('translateY(0%)', 'translateX(50%)', 'translate(25%, 0px)');
+interpolation_test('translateX(50px)', 'translateY(50px)', 'translate(25px, 25px)');
+interpolation_test('translateX(50px)', 'translateZ(50px)', 'translate3d(25px, 0px, 25px)');
+interpolation_test('translateZ(50px)', 'translateX(50px)', 'translate3d(25px, 0px, 25px)');
+interpolation_test('translateZ(-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
+interpolation_test('translate(0%)', 'translate(50%)', 'translate(25%, 0px)');
+interpolation_test('translate(50%)', 'translate(100%, 50%)', 'translate(75%, 25%)');
+interpolation_test('translate(0%, 50%)', 'translate(50%, 100%)', 'translate(25%, 75%)');
+interpolation_test('translate3d(0,0,-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
+interpolation_test('translate(50px, 0px)', 'translate(100px, 0px)', 'translate(75px, 0px)');
+interpolation_test('translate(50px, -50px)', 'translate(100px, 50px)', 'translate(75px, 0px)');
+
+interpolation_test('rotate(30deg)', 'rotate(90deg)', 'rotate(60deg)');
+interpolation_test('rotateZ(30deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 60deg)');
+interpolation_test('rotate(0deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 45deg)');
+interpolation_test('rotateX(0deg)','rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
+interpolation_test('rotate(0deg)', 'rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
+
+interpolation_test('scale(1)', 'scale(2)', 'scale(1.5, 1.5)');
+interpolation_test('scale(1, 3)', 'scale(2)', 'scale(1.5, 2.5)');
+interpolation_test('scaleX(1)', 'scaleX(2)', 'scale(1.5, 1)');
+interpolation_test('scaleY(1)', 'scaleY(2)', 'scale(1, 1.5)');
+interpolation_test('scaleZ(1)', 'scaleZ(2)', 'scale3d(1, 1, 1.5)');
+interpolation_test('scaleX(2)', 'scaleY(2)', 'scale(1.5, 1.5)');
+interpolation_test('scaleX(2)', 'scaleY(3)', 'scale(1.5, 2)');
+interpolation_test('scaleZ(1)', 'scale(2)', 'scale3d(1.5, 1.5, 1)');
+interpolation_test('scale(1, 2)', 'scale(3, 4)', 'scale(2, 3)');
+interpolation_test('scale3d(1, 2, 3)', 'scale3d(4, 5, 6)', 'scale3d(2.5, 3.5, 4.5)');
+interpolation_test('scale3d(1, 2, 3)', 'scale(4, 5)', 'scale3d(2.5, 3.5, 2)');
+interpolation_test('scale(1, 2)', 'scale3d(3, 4, 5)', 'scale3d(2, 3, 3)');
+
+interpolation_test('skewX(0deg)', 'skewX(60deg)', 'skewX(30deg)');
+interpolation_test('skewX(0deg)', 'skewX(90deg)', 'skewX(45deg)');
+interpolation_test('skewX(0deg)', 'skewX(180deg)', 'skewX(90deg)');
+interpolation_test('skew(0deg, 0deg)', 'skew(60deg, 60deg)', 'skew(30deg, 30deg)');
+interpolation_test('skew(45deg, 0deg)', 'skew(0deg, 45deg)', 'skew(22.5deg, 22.5deg)');
+
+interpolation_test('perspective(10px)', 'perspective(2.5px)', 'perspective(4px)');
+interpolation_test('perspective(10px)', 'perspective(none)', 'perspective(20px)');
+interpolation_test('perspective(none)', 'perspective(none)', 'perspective(none)');
+
+// A matrix() with just scale and translation.
+interpolation_test('matrix(2, 0, 0, 2, 10, 30)', 'matrix(4, 0, 0, 6, 14, 10)', 'matrix(3, 0, 0, 4, 12, 20)');
+
+// A matrix3d() with just scale and translation.
+interpolation_test('matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1)', 'matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1)', 'matrix3d(2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 2, 0, -3, 6, 3, 1)');
+// A matrix3d() with just perspective.
+interpolation_test('matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 4, 0, 1, 0, 5, 0, 0, 1, 11, 0, 0, 0, 1)');
+
+// NOTE: New tests added here should also be added in
+// transform-interpolation-inline-value.html.
+
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-inline-value.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-inline-value.html
new file mode 100644
index 0000000000..7b453358cf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-inline-value.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#interpolation-of-transform-functions">
+<meta name="assert" content="transform gives the correct inline values when interpolated">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+
+<body>
+<script>
+function interpolation_test(from, to, expected_50) {
+ test(t => {
+ let div = createDiv(t);
+ let anim = div.animate({transform: [from, to]}, 2000);
+ anim.pause();
+ anim.currentTime = 1000;
+ anim.commitStyles()
+ let halfway = div.style.transform;
+ assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
+ }, "Interpolation between " + from + " and " + to + " gives the correct " +
+ "computed value halfway according to commitStyles.");
+}
+
+interpolation_test('translateX(0px)', 'translateX(50px)', 'translateX(25px)');
+interpolation_test('translateX(0%)', 'translateX(50%)', 'translateX(25%)');
+interpolation_test('translateY(0%)', 'translateX(50%)', 'translate(25%)');
+interpolation_test('translateX(50px)', 'translateY(50px)', 'translate(25px, 25px)');
+interpolation_test('translateX(50px)', 'translateZ(50px)', 'translate3d(25px, 0px, 25px)');
+interpolation_test('translateZ(50px)', 'translateX(50px)', 'translate3d(25px, 0px, 25px)');
+interpolation_test('translateZ(-50px)','translateZ(50px)', 'translateZ(0px)');
+interpolation_test('translate(0%)', 'translate(50%)', 'translate(25%)');
+interpolation_test('translate(50%)', 'translate(100%, 50%)', 'translate(75%, 25%)');
+interpolation_test('translate(0%, 50%)', 'translate(50%, 100%)', 'translate(25%, 75%)');
+interpolation_test('translate3d(0,0,-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
+interpolation_test('translate(50px, 0px)', 'translate(100px, 0px)', 'translate(75px)');
+interpolation_test('translate(50px, -50px)', 'translate(100px, 50px)', 'translate(75px)');
+
+interpolation_test('rotate(30deg)', 'rotate(90deg)', 'rotate(60deg)');
+interpolation_test('rotateZ(30deg)', 'rotateZ(90deg)', 'rotateZ(60deg)');
+interpolation_test('rotate(0deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 45deg)');
+interpolation_test('rotateX(0deg)','rotateX(90deg)', 'rotateX(45deg)');
+interpolation_test('rotate(0deg)', 'rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
+
+interpolation_test('scale(1)', 'scale(2)', 'scale(1.5)');
+interpolation_test('scale(1, 3)', 'scale(2)', 'scale(1.5, 2.5)');
+interpolation_test('scaleX(1)', 'scaleX(2)', 'scaleX(1.5)');
+interpolation_test('scaleY(1)', 'scaleY(2)', 'scaleY(1.5)');
+interpolation_test('scaleZ(1)', 'scaleZ(2)', 'scaleZ(1.5)');
+interpolation_test('scaleX(2)', 'scaleY(2)', 'scale(1.5)');
+interpolation_test('scaleX(2)', 'scaleY(3)', 'scale(1.5, 2)');
+interpolation_test('scaleZ(1)', 'scale(2)', 'scale3d(1.5, 1.5, 1)');
+interpolation_test('scale(1, 2)', 'scale(3, 4)', 'scale(2, 3)');
+interpolation_test('scale3d(1, 2, 3)', 'scale3d(4, 5, 6)', 'scale3d(2.5, 3.5, 4.5)');
+interpolation_test('scale3d(1, 2, 3)', 'scale(4, 5)', 'scale3d(2.5, 3.5, 2)');
+interpolation_test('scale(1, 2)', 'scale3d(3, 4, 5)', 'scale3d(2, 3, 3)');
+
+interpolation_test('skewX(0deg)', 'skewX(60deg)', 'skewX(30deg)');
+interpolation_test('skewX(0deg)', 'skewX(90deg)', 'skewX(45deg)');
+interpolation_test('skewX(0deg)', 'skewX(180deg)', 'skewX(90deg)');
+interpolation_test('skew(0deg, 0deg)', 'skew(60deg, 60deg)', 'skew(30deg, 30deg)');
+interpolation_test('skew(45deg, 0deg)', 'skew(0deg, 45deg)', 'skew(22.5deg, 22.5deg)');
+
+interpolation_test('perspective(10px)', 'perspective(2.5px)', 'perspective(4px)');
+interpolation_test('perspective(10px)', 'perspective(none)', 'perspective(20px)');
+interpolation_test('perspective(none)', 'perspective(none)', 'perspective(none)');
+
+// A matrix() with just scale and translation.
+interpolation_test('matrix(2, 0, 0, 2, 10, 30)', 'matrix(4, 0, 0, 6, 14, 10)', 'matrix(3, 0, 0, 4, 12, 20)');
+
+// A matrix3d() with just scale and translation.
+interpolation_test('matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1)', 'matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1)', 'matrix3d(2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 2, 0, -3, 6, 3, 1)');
+// A matrix3d() with just perspective.
+interpolation_test('matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 4, 0, 1, 0, 5, 0, 0, 1, 11, 0, 0, 0, 1)');
+
+// NOTE: New tests added here should also be added in
+// transform-interpolation-computed-value.html.
+
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-matrix.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-matrix.html
new file mode 100644
index 0000000000..b5f9c3cd30
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-matrix.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?matrix">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.matrix);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-perspective.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-perspective.html
new file mode 100644
index 0000000000..bb1e17b3c3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-perspective.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?perspective">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.perspective);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-ref.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-ref.html
new file mode 100644
index 0000000000..2fee6f7c1f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-ref.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+const testName = window.location.search.substr(1);
+createRefs(transformTests[testName]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate-slerp.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate-slerp.html
new file mode 100644
index 0000000000..73d4bd8a1f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate-slerp.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?rotateSlerp">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+<meta name="fuzzy" content="0-160;0-500">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.rotateSlerp);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate.html
new file mode 100644
index 0000000000..cba2d2086f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-rotate.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?rotate">
+<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-246">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.rotate);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-scale.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-scale.html
new file mode 100644
index 0000000000..ef0da0d1f4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-scale.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?scale">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.scale);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-skew.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-skew.html
new file mode 100644
index 0000000000..95277d4b8c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-skew.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?skew">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+<!--
+Fuzzy match is needed because the browser may raster in different scales for the
+test and the reference. For example, in the first case the test animates skew
+from 0deg to 60deg and take snapshot when the skew is 30deg, while the reference
+animates skew from 30deg to 30deg, and the browser may choose a higher raster
+scale for the former because at 60deg the ideal scale is higher.
+ 3000: 130 * 6 * 2 + 80 * 9 * 2
+ 130, 80: width and height of the div
+ 6: number of antialiased horizontal edges
+ 9: number of antialiased vertical edges
+ 2: number of antialiased pixels for each pixel on the edge
+-->
+<meta name=fuzzy content="0-255;0-3000">
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.skew);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate-em.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate-em.html
new file mode 100644
index 0000000000..aff613ab09
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate-em.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?translateEm">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.translateEm);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate.html
new file mode 100644
index 0000000000..5abeb24093
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-translate.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<link rel="match" href="transform-interpolation-ref.html?translate">
+<link rel="help" href="https://drafts.csswg.org/css-transforms/">
+
+<script src="../../../common/reftest-wait.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<body>
+<script>
+createTests(transformTests.translate);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-verify-reftests.html b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-verify-reftests.html
new file mode 100644
index 0000000000..367b7af306
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-interpolation-verify-reftests.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<meta charset="UTF-8">
+<title>transform interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+<script src="support/transform-interpolation-reftests.js"></script>
+
+<!--
+
+The tests in transform-interpolation-reftests.js are used for reftests
+that are designed to test animation that happens on the compositor.
+Here we run those same tests through test_interpolation to check that
+they match the non-compositor codepath.
+
+-->
+
+<body>
+<script>
+for (const set in transformTests) {
+ for (const obj of transformTests[set]) {
+ let test = ("test" in obj) ? obj.test : obj;
+ let midpoint = ("midpoint" in obj) ? obj.midpoint : 0.5;
+ test_interpolation({
+ property: 'transform',
+ from: test[0],
+ to: test[2]
+ }, [
+ { at: midpoint, expect: test[1] },
+ ]);
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-matrix-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-matrix-composition.html
new file mode 100644
index 0000000000..2586ff3d4a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-matrix-composition.html
@@ -0,0 +1,208 @@
+<!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>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-origin-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/transform-origin-interpolation.html
new file mode 100644
index 0000000000..02b8b59b0b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-origin-interpolation.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform-origin interpolation</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#transform-origin-property">
+<meta name="assert" content="transform supports animation by computed value">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<style>
+.parent {
+ transform-origin: 30px 10px;
+}
+.target {
+ display: inline-block;
+ margin-top: 50px;
+ margin-bottom: 25px;
+ width: 50px;
+ height: 50px;
+ background: red;
+ transform: scale(1.5);
+ transform-origin: 10px 30px;
+}
+.expected {
+ background: green;
+ position: relative;
+ left: -50px;
+}
+</style>
+
+<body></body>
+
+<script>
+test_interpolation({
+ property: 'transform-origin',
+ from: neutralKeyframe,
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '7px 33px'},
+ {at: 0, expect: '10px 30px'},
+ {at: 0.3, expect: '13px 27px'},
+ {at: 0.6, expect: '16px 24px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '25px 15px'},
+]);
+
+test_interpolation({
+ property: 'transform-origin',
+ from: 'initial',
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '26.5px 26.5px'},
+ {at: 0, expect: '25px 25px'},
+ {at: 0.3, expect: '23.5px 23.5px'},
+ {at: 0.6, expect: '22px 22px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '17.5px 17.5px'},
+]);
+
+test_interpolation({
+ property: 'transform-origin',
+ from: 'inherit',
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '33px 7px'},
+ {at: 0, expect: '30px 10px'},
+ {at: 0.3, expect: '27px 13px'},
+ {at: 0.6, expect: '24px 16px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '15px 25px'},
+]);
+
+test_interpolation({
+ property: 'transform-origin',
+ from: 'unset',
+ to: '20px 20px',
+}, [
+ {at: -0.3, expect: '26.5px 26.5px'},
+ {at: 0, expect: '25px 25px'},
+ {at: 0.3, expect: '23.5px 23.5px'},
+ {at: 0.6, expect: '22px 22px'},
+ {at: 1, expect: '20px 20px'},
+ {at: 1.5, expect: '17.5px 17.5px'},
+]);
+
+test_interpolation({
+ property: 'transform-origin',
+ from: 'top left',
+ to: 'bottom right',
+}, [
+ {at: -0.3, expect: '-15px -15px'},
+ {at: 0, expect: '0px 0px'},
+ {at: 0.3, expect: '15px 15px'},
+ {at: 0.6, expect: '30px 30px'},
+ {at: 1, expect: '50px 50px'},
+ {at: 1.5, expect: '75px 75px'},
+]);
+
+test_interpolation({
+ property: 'transform-origin',
+ from: 'center center',
+ to: '0% 100px',
+}, [
+ {at: -0.3, expect: '32.5px 2.5px'},
+ {at: 0, expect: '25px 25px'},
+ {at: 0.3, expect: '17.5px 47.5px'},
+ {at: 0.6, expect: '10px 70px'},
+ {at: 1, expect: '0px 100px'},
+ {at: 1.5, expect: '-12.5px 137.5px'},
+]);
+
+test_interpolation({
+ property: 'transform-origin',
+ from: '0% 50% 5px',
+ to: '100% 150% 0px'
+}, [
+ {at: -0.3, expect: '-30% 20% 6.5px'},
+ {at: 0, expect: '0% 50% 5px'},
+ {at: 0.3, expect: '30% 80% 3.5px'},
+ {at: 0.6, expect: '60% 110% 2px'},
+ {at: 1, expect: '100% 150% 0px'},
+ {at: 1.5, expect: '150% 200% -2.5px'},
+]);
+</script>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-perspective-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-perspective-composition.html
new file mode 100644
index 0000000000..1266f081e6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-perspective-composition.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform-perspective composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#three-d-transform-functions">
+<meta name="assert" content="transform-perspective 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>
+// Addition and accumulation of perspective values are very similar, but not
+// identical. We can test the difference by constructing a scenario where a
+// perspective parameter would go negative in one case (and thus be clamped
+// to 0), and would not go negative in the other case.
+//
+// In the test below, the values differ at 1.5 progress. The reason for this
+// is that at 1.5 progress, the addition case (which uses concatenation)
+// computes to:
+//
+// perspective(10px) perspective(-50px)
+//
+// Since perspective cannot go negative, this is clamped to:
+//
+// perspective(10px) perspective(none)
+//
+// The accumulation case, on the other hand, combines the components
+// and so ends up blending from perspective(5px) to perspective(8.33...px) at
+// 1.5 progress, which results in perspective(12.5px) - this is what you would
+// get with addition too, if not for the clamping behavior.
+
+// ------------ Addition tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'perspective(10px)',
+ addFrom: 'perspective(10px)',
+ addTo: 'perspective(50px)',
+}, [
+ {at: -0.5, expect: 'perspective(4.12px)'},
+ {at: 0, expect: 'perspective(5px)'},
+ {at: 0.25, expect: 'perspective(5.45px)'},
+ {at: 0.5, expect: 'perspective(6.15px)'},
+ {at: 0.75, expect: 'perspective(7.06px)'},
+ {at: 1, expect: 'perspective(8.33px)'},
+ {at: 1.5, expect: 'perspective(10px)'},
+]);
+
+// ------------ Accumulation tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'perspective(10px)',
+ accumulateFrom: 'perspective(10px)',
+ accumulateTo: 'perspective(50px)',
+}, [
+ {at: -0.5, expect: 'perspective(4.12px)'},
+ {at: 0, expect: 'perspective(5px)'},
+ {at: 0.25, expect: 'perspective(5.45px)'},
+ {at: 0.5, expect: 'perspective(6.15px)'},
+ {at: 0.75, expect: 'perspective(7.06px)'},
+ {at: 1, expect: 'perspective(8.33px)'},
+ {at: 1.5, expect: 'perspective(12.5px)'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-rotate-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-rotate-composition.html
new file mode 100644
index 0000000000..e062860e72
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-rotate-composition.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform-rotate composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#funcdef-transform-rotate">
+<meta name="assert" content="transform-rotate 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>
+// ------------ Addition tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(20deg)',
+ addFrom: 'rotateX(40deg)',
+ addTo: 'rotateX(60deg)',
+}, [
+ {at: -0.5, expect: 'rotateX(50deg)'},
+ {at: 0, expect: 'rotateX(60deg)'},
+ {at: 0.25, expect: 'rotateX(65deg)'},
+ {at: 0.5, expect: 'rotateX(70deg)'},
+ {at: 0.75, expect: 'rotateX(75deg)'},
+ {at: 1, expect: 'rotateX(80deg)'},
+ {at: 1.5, expect: 'rotateX(90deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'rotateY(20deg)',
+ addFrom: 'rotateY(40deg)',
+ addTo: 'rotateY(60deg)',
+}, [
+ {at: -0.5, expect: 'rotateY(50deg)'},
+ {at: 0, expect: 'rotateY(60deg)'},
+ {at: 0.25, expect: 'rotateY(65deg)'},
+ {at: 0.5, expect: 'rotateY(70deg)'},
+ {at: 0.75, expect: 'rotateY(75deg)'},
+ {at: 1, expect: 'rotateY(80deg)'},
+ {at: 1.5, expect: 'rotateY(90deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'rotateZ(20deg)',
+ addFrom: 'rotateZ(40deg)',
+ addTo: 'rotateZ(60deg)',
+}, [
+ {at: -0.5, expect: 'rotateZ(50deg)'},
+ {at: 0, expect: 'rotateZ(60deg)'},
+ {at: 0.25, expect: 'rotateZ(65deg)'},
+ {at: 0.5, expect: 'rotateZ(70deg)'},
+ {at: 0.75, expect: 'rotateZ(75deg)'},
+ {at: 1, expect: 'rotateZ(80deg)'},
+ {at: 1.5, expect: 'rotateZ(90deg)'},
+]);
+
+// When testing rotate functions in isolation, the additive and accumulation
+// behaviors are functionally identical. This test includes a skew to ensure
+// both methods are implemented; add should append the from/to after the skew.
+test_composition({
+ property: 'transform',
+ underlying: 'rotate(45deg) skew(10deg, 20deg)',
+ addFrom: 'rotate(45deg)',
+ addTo: 'rotate(225deg)',
+}, [
+ {at: -0.5, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(-45deg)'},
+ {at: 0, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(45deg)'},
+ {at: 0.25, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(90deg)'},
+ {at: 0.5, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(135deg)'},
+ {at: 0.75, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(180deg)'},
+ {at: 1, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(225deg)'},
+ {at: 1.5, expect: 'rotate(45deg) skew(10deg, 20deg) rotate(315deg)'},
+]);
+// ------------ Accumulation tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(20deg)',
+ accumulateFrom: 'rotateX(40deg)',
+ accumulateTo: 'rotateX(60deg)',
+}, [
+ {at: -0.5, expect: 'rotateX(50deg)'},
+ {at: 0, expect: 'rotateX(60deg)'},
+ {at: 0.25, expect: 'rotateX(65deg)'},
+ {at: 0.5, expect: 'rotateX(70deg)'},
+ {at: 0.75, expect: 'rotateX(75deg)'},
+ {at: 1, expect: 'rotateX(80deg)'},
+ {at: 1.5, expect: 'rotateX(90deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'rotateY(20deg)',
+ accumulateFrom: 'rotateY(40deg)',
+ accumulateTo: 'rotateY(60deg)',
+}, [
+ {at: -0.5, expect: 'rotateY(50deg)'},
+ {at: 0, expect: 'rotateY(60deg)'},
+ {at: 0.25, expect: 'rotateY(65deg)'},
+ {at: 0.5, expect: 'rotateY(70deg)'},
+ {at: 0.75, expect: 'rotateY(75deg)'},
+ {at: 1, expect: 'rotateY(80deg)'},
+ {at: 1.5, expect: 'rotateY(90deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'rotateZ(20deg)',
+ accumulateFrom: 'rotateZ(40deg)',
+ accumulateTo: 'rotateZ(60deg)',
+}, [
+ {at: -0.5, expect: 'rotateZ(50deg)'},
+ {at: 0, expect: 'rotateZ(60deg)'},
+ {at: 0.25, expect: 'rotateZ(65deg)'},
+ {at: 0.5, expect: 'rotateZ(70deg)'},
+ {at: 0.75, expect: 'rotateZ(75deg)'},
+ {at: 1, expect: 'rotateZ(80deg)'},
+ {at: 1.5, expect: 'rotateZ(90deg)'},
+]);
+
+// The rotate functions all share the same primitive type (rotate3d), so can be
+// accumulated between. If primitive type matching is not properly being
+// performed, this test would likely fail with a fallback to replace behavior.
+test_composition({
+ property: 'transform',
+ underlying: 'rotateX(45deg)',
+ accumulateFrom: 'rotateY(30deg)',
+ accumulateTo: 'rotateY(70deg)',
+}, [
+ // Due to how rotation is accumulated (addition of underlying angles), the
+ // behavior is identical to concatenating the components. The expectations
+ // are expressed as concatenations for readability.
+ {at: -0.5, expect: 'rotateX(45deg) rotateY(10deg)'},
+ {at: 0, expect: 'rotateX(45deg) rotateY(30deg)'},
+ {at: 0.25, expect: 'rotateX(45deg) rotateY(40deg)'},
+ {at: 0.5, expect: 'rotateX(45deg) rotateY(50deg)'},
+ {at: 0.75, expect: 'rotateX(45deg) rotateY(60deg)'},
+ {at: 1, expect: 'rotateX(45deg) rotateY(70deg)'},
+ {at: 1.5, expect: 'rotateX(45deg) rotateY(90deg)'},
+]);
+
+// When testing rotate functions in isolation, the additive and accumulation
+// behaviors are functionally identical. This test includes a skew to ensure
+// both methods are implemented; accumulate should combine the rotate before
+// the skew.
+test_composition({
+ property: 'transform',
+ underlying: 'rotate(45deg) skew(10deg, 20deg)',
+ accumulateFrom: 'rotate(45deg)',
+ accumulateTo: 'rotate(225deg)',
+}, [
+ {at: -0.5, expect: 'rotate(0deg) skew(10deg, 20deg)'},
+ {at: 0, expect: 'rotate(90deg) skew(10deg, 20deg)'},
+ {at: 0.25, expect: 'rotate(135deg) skew(10deg, 20deg)'},
+ {at: 0.5, expect: 'rotate(180deg) skew(10deg, 20deg)'},
+ {at: 0.75, expect: 'rotate(225deg) skew(10deg, 20deg)'},
+ {at: 1, expect: 'rotate(270deg) skew(10deg, 20deg)'},
+ {at: 1.5, expect: 'rotate(360deg) skew(10deg, 20deg)'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-scale-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-scale-composition.html
new file mode 100644
index 0000000000..87c33a85ed
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-scale-composition.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform-scale composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#two-d-transform-functions">
+<meta name="assert" content="transform-scale 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>
+// Addition (aka concatenation) of scale functions results in multiplying their
+// values (scale(2) scale(3) == scale(6)), whereas accumulation does a 1-based
+// sum of the components (accumulate(scale(2), scale(3)) == scale(2 + 3 - 1) ==
+// scale(4)).
+
+// ------------ Addition tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'scaleX(2)',
+ addFrom: 'scaleX(3)',
+ addTo: 'scaleX(4)',
+}, [
+ {at: -0.5, expect: 'scaleX(5)'},
+ {at: 0, expect: 'scaleX(6)'},
+ {at: 0.25, expect: 'scaleX(6.5)'},
+ {at: 0.5, expect: 'scaleX(7)'},
+ {at: 0.75, expect: 'scaleX(7.5)'},
+ {at: 1, expect: 'scaleX(8)'},
+ {at: 1.5, expect: 'scaleX(9)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'scaleY(2)',
+ addFrom: 'scaleY(3)',
+ addTo: 'scaleY(4)',
+}, [
+ {at: -0.5, expect: 'scaleY(5)'},
+ {at: 0, expect: 'scaleY(6)'},
+ {at: 0.25, expect: 'scaleY(6.5)'},
+ {at: 0.5, expect: 'scaleY(7)'},
+ {at: 0.75, expect: 'scaleY(7.5)'},
+ {at: 1, expect: 'scaleY(8)'},
+ {at: 1.5, expect: 'scaleY(9)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'scaleZ(2)',
+ addFrom: 'scaleZ(3)',
+ addTo: 'scaleZ(4)',
+}, [
+ {at: -0.5, expect: 'scaleZ(5)'},
+ {at: 0, expect: 'scaleZ(6)'},
+ {at: 0.25, expect: 'scaleZ(6.5)'},
+ {at: 0.5, expect: 'scaleZ(7)'},
+ {at: 0.75, expect: 'scaleZ(7.5)'},
+ {at: 1, expect: 'scaleZ(8)'},
+ {at: 1.5, expect: 'scaleZ(9)'},
+]);
+
+// ------------ Accumulation tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'scaleX(2)',
+ accumulateFrom: 'scaleX(3)',
+ accumulateTo: 'scaleX(4)',
+}, [
+ {at: -0.5, expect: 'scaleX(3.5)'},
+ {at: 0, expect: 'scaleX(4)'},
+ {at: 0.25, expect: 'scaleX(4.25)'},
+ {at: 0.5, expect: 'scaleX(4.5)'},
+ {at: 0.75, expect: 'scaleX(4.75)'},
+ {at: 1, expect: 'scaleX(5)'},
+ {at: 1.5, expect: 'scaleX(5.5)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'scaleY(2)',
+ accumulateFrom: 'scaleY(3)',
+ accumulateTo: 'scaleY(4)',
+}, [
+ {at: -0.5, expect: 'scaleY(3.5)'},
+ {at: 0, expect: 'scaleY(4)'},
+ {at: 0.25, expect: 'scaleY(4.25)'},
+ {at: 0.5, expect: 'scaleY(4.5)'},
+ {at: 0.75, expect: 'scaleY(4.75)'},
+ {at: 1, expect: 'scaleY(5)'},
+ {at: 1.5, expect: 'scaleY(5.5)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'scaleZ(2)',
+ accumulateFrom: 'scaleZ(3)',
+ accumulateTo: 'scaleZ(4)',
+}, [
+ {at: -0.5, expect: 'scaleZ(3.5)'},
+ {at: 0, expect: 'scaleZ(4)'},
+ {at: 0.25, expect: 'scaleZ(4.25)'},
+ {at: 0.5, expect: 'scaleZ(4.5)'},
+ {at: 0.75, expect: 'scaleZ(4.75)'},
+ {at: 1, expect: 'scaleZ(5)'},
+ {at: 1.5, expect: 'scaleZ(5.5)'},
+]);
+
+// The scale functions all share the same primitive type (scale3d), so can be
+// accumulated between.
+test_composition({
+ property: 'transform',
+ underlying: 'scale(2, 4)',
+ accumulateFrom: 'scaleZ(3)',
+ accumulateTo: 'scaleZ(4)',
+}, [
+ {at: -0.5, expect: 'scale3d(2, 4, 2.5)'},
+ {at: 0, expect: 'scale3d(2, 4, 3)'},
+ {at: 0.25, expect: 'scale3d(2, 4, 3.25)'},
+ {at: 0.5, expect: 'scale3d(2, 4, 3.5)'},
+ {at: 0.75, expect: 'scale3d(2, 4, 3.75)'},
+ {at: 1, expect: 'scale3d(2, 4, 4)'},
+ {at: 1.5, expect: 'scale3d(2, 4, 4.5)'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-skew-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-skew-composition.html
new file mode 100644
index 0000000000..cda44b60c0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-skew-composition.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform-skew composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#two-d-transform-functions">
+<meta name="assert" content="transform-skew 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>
+// Addition (aka concatenation) of two skew functions skew(a) and skew(b)
+// results in computing tan(a) + tan(b), whereas accumulation results in summing
+// the components to get tan(a + b).
+
+// ------------ Addition tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'skewX(10deg)',
+ addFrom: 'skewX(30deg)',
+ addTo: 'skewX(50deg)',
+}, [
+ {at: -0.5, expect: 'skewX(10deg) skewX(20deg)'},
+ {at: 0, expect: 'skewX(10deg) skewX(30deg)'},
+ {at: 0.25, expect: 'skewX(10deg) skewX(35deg)'},
+ {at: 0.5, expect: 'skewX(10deg) skewX(40deg)'},
+ {at: 0.75, expect: 'skewX(10deg) skewX(45deg)'},
+ {at: 1, expect: 'skewX(10deg) skewX(50deg)'},
+ {at: 1.5, expect: 'skewX(10deg) skewX(60deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'skewY(10deg)',
+ addFrom: 'skewY(30deg)',
+ addTo: 'skewY(50deg)',
+}, [
+ {at: -0.5, expect: 'skewY(10deg) skewY(20deg)'},
+ {at: 0, expect: 'skewY(10deg) skewY(30deg)'},
+ {at: 0.25, expect: 'skewY(10deg) skewY(35deg)'},
+ {at: 0.5, expect: 'skewY(10deg) skewY(40deg)'},
+ {at: 0.75, expect: 'skewY(10deg) skewY(45deg)'},
+ {at: 1, expect: 'skewY(10deg) skewY(50deg)'},
+ {at: 1.5, expect: 'skewY(10deg) skewY(60deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'skew(10deg, 20deg)',
+ addFrom: 'skew(30deg, 10deg)',
+ addTo: 'skew(50deg, 50deg)',
+}, [
+ {at: -0.5, expect: 'skew(10deg, 20deg) skew(20deg, -10deg)'},
+ {at: 0, expect: 'skew(10deg, 20deg) skew(30deg, 10deg)'},
+ {at: 0.25, expect: 'skew(10deg, 20deg) skew(35deg, 20deg)'},
+ {at: 0.5, expect: 'skew(10deg, 20deg) skew(40deg, 30deg)'},
+ {at: 0.75, expect: 'skew(10deg, 20deg) skew(45deg, 40deg)'},
+ {at: 1, expect: 'skew(10deg, 20deg) skew(50deg, 50deg)'},
+ {at: 1.5, expect: 'skew(10deg, 20deg) skew(60deg, 70deg)'},
+]);
+
+// ------------ Accumulation tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'skewX(45deg)',
+ accumulateFrom: 'skewX(30deg)',
+ accumulateTo: 'skewX(70deg)',
+}, [
+ {at: -0.5, expect: 'skewX(55deg)'},
+ {at: 0, expect: 'skewX(75deg)'},
+ {at: 0.25, expect: 'skewX(85deg)'},
+ {at: 0.5, expect: 'skewX(95deg)'},
+ {at: 0.75, expect: 'skewX(105deg)'},
+ {at: 1, expect: 'skewX(115deg)'},
+ {at: 1.5, expect: 'skewX(135deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'skewY(45deg)',
+ accumulateFrom: 'skewY(30deg)',
+ accumulateTo: 'skewY(70deg)',
+}, [
+ {at: -0.5, expect: 'skewY(55deg)'},
+ {at: 0, expect: 'skewY(75deg)'},
+ {at: 0.25, expect: 'skewY(85deg)'},
+ {at: 0.5, expect: 'skewY(95deg)'},
+ {at: 0.75, expect: 'skewY(105deg)'},
+ {at: 1, expect: 'skewY(115deg)'},
+ {at: 1.5, expect: 'skewY(135deg)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'skew(10deg, 45deg)',
+ accumulateFrom: 'skew(20deg, 30deg)',
+ accumulateTo: 'skew(40deg, 70deg)',
+}, [
+ {at: -0.5, expect: 'skew(20deg, 55deg)'},
+ {at: 0, expect: 'skew(30deg, 75deg)'},
+ {at: 0.25, expect: 'skew(35deg, 85deg)'},
+ {at: 0.5, expect: 'skew(40deg, 95deg)'},
+ {at: 0.75, expect: 'skew(45deg, 105deg)'},
+ {at: 1, expect: 'skew(50deg, 115deg)'},
+ {at: 1.5, expect: 'skew(60deg, 135deg)'},
+]);
+
+// The skew{X,Y} functions DO NOT share the same primitive type, so cannot be
+// accumlated between directly. Instead, they fall back to matrix accumulation,
+// which this tests for.
+test_composition({
+ property: 'transform',
+ underlying: 'skewX(45deg)',
+ accumulateFrom: 'skewY(45deg)',
+ accumulateTo: 'skewY(45deg)',
+}, [
+ // Note that this is not equivalent to any form of combined skews.
+ {at: 0.5, expect: 'matrix(1, 1, 0.5, 1.5, 0, 0)'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/transform-translate-composition.html b/testing/web-platform/tests/css/css-transforms/animation/transform-translate-composition.html
new file mode 100644
index 0000000000..49214c4a93
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/transform-translate-composition.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<title>transform-translate composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms/#two-d-transform-functions">
+<meta name="assert" content="transform-translate 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 src="../interpolation/resources/interpolation-test.js"></script>
+<script>
+// ------------ Addition tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'translate(10px, 20px)',
+ addFrom: 'translate(100px, 200px)',
+ addTo: 'translate(200px, 400px)',
+}, [
+ {at: -0.5, expect: 'translate(60px, 120px)'},
+ {at: 0, expect: 'translate(110px, 220px)'},
+ {at: 0.25, expect: 'translate(135px, 270px)'},
+ {at: 0.5, expect: 'translate(160px, 320px)'},
+ {at: 0.75, expect: 'translate(185px, 370px)'},
+ {at: 1, expect: 'translate(210px, 420px)'},
+ {at: 1.5, expect: 'translate(260px, 520px)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'translate(10px, 20px)',
+ addFrom: 'translate(100px, 200px)',
+ replaceTo: 'translate(210px, 420px)',
+}, [
+ {at: -0.5, expect: 'translate(60px, 120px)'},
+ {at: 0, expect: 'translate(110px, 220px)'},
+ {at: 0.25, expect: 'translate(135px, 270px)'},
+ {at: 0.5, expect: 'translate(160px, 320px)'},
+ {at: 0.75, expect: 'translate(185px, 370px)'},
+ {at: 1, expect: 'translate(210px, 420px)'},
+ {at: 1.5, expect: 'translate(260px, 520px)'},
+]);
+
+// When testing translate functions in isolation, the additive and accumulation
+// behaviors are functionally identical. This test includes a rotate to ensure
+// both methods are implemented; add should append the from/to after the rotate.
+test_composition({
+ property: 'transform',
+ underlying: 'translateX(100px) rotate(90deg)',
+ addFrom: 'translateX(100px)',
+ addTo: 'translateX(200px)',
+}, [
+ {at: -0.5, expect: 'translateX(100px) rotate(90deg) translateX(50px)'},
+ {at: 0, expect: 'translateX(100px) rotate(90deg) translateX(100px)'},
+ {at: 0.25, expect: 'translateX(100px) rotate(90deg) translateX(125px)'},
+ {at: 0.5, expect: 'translateX(100px) rotate(90deg) translateX(150px)'},
+ {at: 0.75, expect: 'translateX(100px) rotate(90deg) translateX(175px)'},
+ {at: 1, expect: 'translateX(100px) rotate(90deg) translateX(200px)'},
+ {at: 1.5, expect: 'translateX(100px) rotate(90deg) translateX(250px)'},
+]);
+
+// ------------ Accumulation tests --------------
+
+test_composition({
+ property: 'transform',
+ underlying: 'translateX(100px)',
+ accumulateFrom: 'translateX(50px)',
+ accumulateTo: 'translateX(250px)',
+}, [
+ {at: -0.5, expect: 'translateX(50px)'},
+ {at: 0, expect: 'translateX(150px)'},
+ {at: 0.25, expect: 'translateX(200px)'},
+ {at: 0.5, expect: 'translateX(250px)'},
+ {at: 0.75, expect: 'translateX(300px)'},
+ {at: 1, expect: 'translateX(350px)'},
+ {at: 1.5, expect: 'translateX(450px)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'translateY(100px)',
+ accumulateFrom: 'translateY(50px)',
+ accumulateTo: 'translateY(250px)',
+}, [
+ {at: -0.5, expect: 'translateY(50px)'},
+ {at: 0, expect: 'translateY(150px)'},
+ {at: 0.25, expect: 'translateY(200px)'},
+ {at: 0.5, expect: 'translateY(250px)'},
+ {at: 0.75, expect: 'translateY(300px)'},
+ {at: 1, expect: 'translateY(350px)'},
+ {at: 1.5, expect: 'translateY(450px)'},
+]);
+
+test_composition({
+ property: 'transform',
+ underlying: 'translateZ(100px)',
+ accumulateFrom: 'translateZ(50px)',
+ accumulateTo: 'translateZ(250px)',
+}, [
+ {at: -0.5, expect: 'translateZ(50px)'},
+ {at: 0, expect: 'translateZ(150px)'},
+ {at: 0.25, expect: 'translateZ(200px)'},
+ {at: 0.5, expect: 'translateZ(250px)'},
+ {at: 0.75, expect: 'translateZ(300px)'},
+ {at: 1, expect: 'translateZ(350px)'},
+ {at: 1.5, expect: 'translateZ(450px)'},
+]);
+
+// The translate functions all share the same primitive type (translate3d), so
+// can be accumulated between.
+test_composition({
+ property: 'transform',
+ underlying: 'translate(100px, 50px)',
+ accumulateFrom: 'translateZ(50px)',
+ accumulateTo: 'translateZ(250px)',
+}, [
+ {at: -0.5, expect: 'translate3d(100px, 50px, -50px)'},
+ {at: 0, expect: 'translate3d(100px, 50px, 50px)'},
+ {at: 0.25, expect: 'translate3d(100px, 50px, 100px)'},
+ {at: 0.5, expect: 'translate3d(100px, 50px, 150px)'},
+ {at: 0.75, expect: 'translate3d(100px, 50px, 200px)'},
+ {at: 1, expect: 'translate3d(100px, 50px, 250px)'},
+ {at: 1.5, expect: 'translate3d(100px, 50px, 350px)'},
+]);
+
+// When testing translate functions in isolation, the additive and accumulation
+// behaviors are functionally identical. This test includes a rotate to ensure
+// both methods are implemented; accumulate should combine the transform before
+// the rotate.
+test_composition({
+ property: 'transform',
+ underlying: 'translateX(100px) rotate(90deg)',
+ accumulateFrom: 'translateX(100px)',
+ accumulateTo: 'translateX(200px)',
+}, [
+ {at: -0.5, expect: 'translateX(150px) rotate(90deg)'},
+ {at: 0, expect: 'translateX(200px) rotate(90deg)'},
+ {at: 0.25, expect: 'translateX(225px) rotate(90deg)'},
+ {at: 0.5, expect: 'translateX(250px) rotate(90deg)'},
+ {at: 0.75, expect: 'translateX(275px) rotate(90deg)'},
+ {at: 1, expect: 'translateX(300px) rotate(90deg)'},
+ {at: 1.5, expect: 'translateX(350px) rotate(90deg)'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg-ref.html b/testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg-ref.html
new file mode 100644
index 0000000000..d503f33851
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg-ref.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Animating the "translate" property on an SVG element</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+
+<style>
+
+svg {
+ width: 400px;
+ height: 400px;
+}
+
+rect {
+ width: 200px;
+ height: 200px;
+ transform-origin: top left;
+ translate: 100px 100px;
+}
+
+</style>
+</head>
+<body>
+<svg><rect></rect></svg>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg.html b/testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg.html
new file mode 100644
index 0000000000..e5cc697360
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/translate-animation-on-svg.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<title>Animating the "translate" property on an SVG element</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#individual-transforms">
+<link rel="match" href="translate-animation-on-svg-ref.html">
+
+<style>
+
+@keyframes translate-animation {
+ from { translate: 0 0; }
+ to { translate: 100px 100px; }
+}
+
+svg {
+ width: 400px;
+ height: 400px;
+}
+
+rect {
+ width: 100px;
+ height: 100px;
+ transform-origin: top left;
+ animation: translate-animation 1ms linear forwards;
+}
+
+</style>
+</head>
+<body>
+<svg><rect></rect></svg>
+
+<script>
+
+(async function() {
+ await Promise.all(document.getAnimations().map(animation => animation.finished));
+ document.documentElement.classList.remove("reftest-wait");
+})();
+
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-transforms/animation/translate-composition.html b/testing/web-platform/tests/css/css-transforms/animation/translate-composition.html
new file mode 100644
index 0000000000..3250d746a3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/translate-composition.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>translate composition</title>
+<link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-translate">
+<meta name="assert" content="translate supports <length> and <percentage> animation.">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/interpolation-testcommon.js"></script>
+
+<body>
+<style>
+.target {
+ width: 100px;
+ height: 100px;
+}
+</style>
+<script>
+test_composition({
+ property: 'translate',
+ underlying: '100px 200px 300px',
+ addFrom: '-50px 50%',
+ addTo: '100%',
+}, [
+ {at: -1, expect: '-100% calc(200px + 100%) 300px'},
+ {at: 0, expect: 'calc(50px + 0%) calc(200px + 50%) 300px'},
+ {at: 0.25, expect: 'calc(62.5px + 25%) calc(200px + 37.5%) 300px'},
+ {at: 0.75, expect: 'calc(87.5px + 75%) calc(200px + 12.5%) 300px'},
+ {at: 1, expect: 'calc(100px + 100%) calc(200px + 0%) 300px'},
+ {at: 2, expect: 'calc(150px + 200%) calc(200px - 50%) 300px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: '100px 200px 300px',
+ addFrom: '50% 100px',
+ replaceTo: '200px 50% 100px',
+}, [
+ {at: -1, expect: '100% calc(600px - 50%) 500px'},
+ {at: 0, expect: 'calc(100px + 50%) calc(300px + 0%) 300px'},
+ {at: 0.25, expect: 'calc(125px + 37.5%) calc(225px + 12.5%) 250px'},
+ {at: 0.75, expect: 'calc(175px + 12.5%) calc(75px + 37.5%) 150px'},
+ {at: 1, expect: 'calc(200px + 0%) 50% 100px'},
+ {at: 2, expect: 'calc(300px - 50%) calc(-300px + 100%) -100px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: '100px 200px 300px',
+ replaceFrom: '50% 100px',
+ addTo: '200px 50% 100px',
+}, [
+ {at: -1, expect: 'calc(-300px + 100%) -50% -400px'},
+ {at: 0, expect: '50% calc(100px + 0%)'},
+ {at: 0.25, expect: 'calc(75px + 37.5%) calc(125px + 12.5%) 100px'},
+ {at: 0.75, expect: 'calc(225px + 12.5%) calc(175px + 37.5%) 300px'},
+ {at: 1, expect: 'calc(300px + 0%) calc(200px + 50%) 400px'},
+ {at: 2, expect: 'calc(600px - 50%) calc(300px + 100%) 800px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: 'none',
+ replaceFrom: 'none',
+ addTo: '100px',
+}, [
+ {at: -1, expect: '-100px'},
+ {at: 0, expect: '0px'},
+ {at: 0.25, expect: '25px'},
+ {at: 0.75, expect: '75px'},
+ {at: 1, expect: '100px'},
+ {at: 2, expect: '200px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: 'none',
+ addFrom: 'none',
+ addTo: '100px',
+}, [
+ {at: -1, expect: '-100px'},
+ {at: 0, expect: '0px'},
+ {at: 0.25, expect: '25px'},
+ {at: 0.75, expect: '75px'},
+ {at: 1, expect: '100px'},
+ {at: 2, expect: '200px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: 'none',
+ replaceFrom: '0px 40px 60px',
+ replaceTo: 'none',
+}, [
+ {at: -1, expect: '0px 80px 120px'},
+ {at: 0, expect: '0px 40px 60px'},
+ {at: 0.25, expect: '0px 30px 45px'},
+ {at: 0.75, expect: '0px 10px 15px'},
+ {at: 1, expect: '0px'},
+ {at: 2, expect: '0px -40px -60px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: 'none',
+ replaceFrom: '0px 40px 60px',
+ addTo: 'none',
+}, [
+ {at: -1, expect: '0px 80px 120px'},
+ {at: 0, expect: '0px 40px 60px'},
+ {at: 0.25, expect: '0px 30px 45px'},
+ {at: 0.75, expect: '0px 10px 15px'},
+ {at: 1, expect: '0px'},
+ {at: 2, expect: '0px -40px -60px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: '80px 20px',
+ addFrom: 'none',
+ replaceTo: '0px 40px 60px',
+}, [
+ {at: -1, expect: '160px 0px -60px'},
+ {at: 0, expect: '80px 20px'},
+ {at: 0.25, expect: '60px 25px 15px'},
+ {at: 0.5, expect: '40px 30px 30px'},
+ {at: 0.75, expect: '20px 35px 45px'},
+ {at: 1, expect: '0px 40px 60px'},
+ {at: 2, expect: '-80px 60px 120px'},
+]);
+
+test_composition({
+ property: 'translate',
+ underlying: '80px 20px',
+ addFrom: '0px 40px 60px',
+ replaceTo: 'none',
+}, [
+ {at: -1, expect: '160px 120px 120px'},
+ {at: 0, expect: '80px 60px 60px'},
+ {at: 0.25, expect: '60px 45px 45px'},
+ {at: 0.5, expect: '40px 30px 30px'},
+ {at: 0.75, expect: '20px 15px 15px'},
+ {at: 1, expect: '0px'},
+ {at: 2, expect: '-80px -60px -60px'},
+]);
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-transforms/animation/translate-interpolation.html b/testing/web-platform/tests/css/css-transforms/animation/translate-interpolation.html
new file mode 100644
index 0000000000..4ece9721b8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-transforms/animation/translate-interpolation.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>translate interpolation</title>
+ <link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-translate">
+ <meta name="assert" content="translate supports <length> and <percentage> animation.">
+ <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>
+ .parent {
+ translate: 100px 200px 300px;
+ }
+ .target {
+ width: 100px;
+ height: 100px;
+ background-color: black;
+ translate: 10px;
+ }
+ .expected {
+ background-color: green;
+ }
+ </style>
+ </head>
+ <body>
+ <template id="target-template">
+ <div class="parent">
+ <div class="target"></div>
+ </div>
+ </template>
+ <script>
+ // Matching one-length (pixels) animation.
+ test_interpolation({
+ property: 'translate',
+ from: '-100px',
+ to: '100px',
+ }, [
+ {at: -1, expect: '-300px'},
+ {at: 0, expect: '-100px'},
+ {at: 0.25, expect: '-50px'},
+ {at: 0.75, expect: '50px'},
+ {at: 1, expect: '100px'},
+ {at: 2, expect: '300px'},
+ ]);
+
+ // Matching one-length (percentage) animation.
+ test_interpolation({
+ property: 'translate',
+ from: '-100%',
+ to: '100%',
+ }, [
+ {at: -1, expect: '-300%'},
+ {at: 0, expect: '-100%'},
+ {at: 0.25, expect: '-50%'},
+ {at: 0.75, expect: '50%'},
+ {at: 1, expect: '100%'},
+ {at: 2, expect: '300%'},
+ ]);
+
+ // Matching two-length (all pixels) animation.
+ test_interpolation({
+ property: 'translate',
+ from: '-100px -50px',
+ to: '100px 50px',
+ }, [
+ {at: -1, expect: '-300px -150px'},
+ {at: 0, expect: '-100px -50px'},
+ {at: 0.25, expect: '-50px -25px'},
+ {at: 0.75, expect: '50px 25px'},
+ {at: 1, expect: '100px 50px'},
+ {at: 2, expect: '300px 150px'},
+ ]);
+
+ // Matching three-length (all pixels) animation.
+ test_interpolation({
+ property: 'translate',
+ from: '220px 240px 260px',
+ to: '300px 400px 500px',
+ }, [
+ {at: -1, expect: '140px 80px 20px'},
+ {at: 0, expect: '220px 240px 260px'},
+ {at: 0.125, expect: '230px 260px 290px'},
+ {at: 0.875, expect: '290px 380px 470px'},
+ {at: 1, expect: '300px 400px 500px'},
+ {at: 2, expect: '380px 560px 740px'}
+ ]);
+
+ // Going from one length to three lengths.
+ test_interpolation({
+ property: 'translate',
+ from: '0px',
+ to: '-100px -50px 100px',
+ }, [
+ {at: -1, expect: '100px 50px -100px'},
+ {at: 0, expect: '0px'},
+ {at: 0.25, expect: '-25px -12.5px 25px'},
+ {at: 0.75, expect: '-75px -37.5px 75px'},
+ {at: 1, expect: '-100px -50px 100px'},
+ {at: 2, expect: '-200px -100px 200px'},
+ ]);
+
+ // Going from three lengths to one length.
+ test_interpolation({
+ property: 'translate',
+ from: '-100px -50px 100px',
+ to: '0px',
+ }, [
+ {at: -1, expect: '-200px -100px 200px'},
+ {at: 0, expect: '-100px -50px 100px'},
+ {at: 0.25, expect: '-75px -37.5px 75px'},
+ {at: 0.75, expect: '-25px -12.5px 25px'},
+ {at: 1, expect: '0px'},
+ {at: 2, expect: '100px 50px -100px'},
+ ]);
+
+ // Going from three-lengths to two-percentages.
+ test_interpolation({
+ property: 'translate',
+ from: '480px 400px 320px',
+ to: '240% 160%',
+ }, [
+ {at: -1, expect: 'calc(960px - 240%) calc(800px - 160%) 640px'},
+ {at: 0, expect: 'calc(0% + 480px) calc(0% + 400px) 320px'},
+ {at: 0.125, expect: 'calc(420px + 30%) calc(350px + 20%) 280px'},
+ {at: 0.875, expect: 'calc(210% + 60px) calc(140% + 50px) 40px'},
+ {at: 1, expect: '240% 160%'},
+ {at: 2, expect: 'calc(480% - 480px) calc(320% - 400px) -320px'}
+ ]);
+
+ // Handling of the none value.
+ test_interpolation({
+ property: 'translate',
+ from: 'none',
+ to: 'none',
+ }, [
+ {at: -1, expect: 'none'},
+ {at: 0, expect: 'none'},
+ {at: 0.125, expect: 'none'},
+ {at: 0.875, expect: 'none'},
+ {at: 1, expect: 'none'},
+ {at: 2, expect: 'none'}
+ ]);
+
+ // Going from none to a valid value; test that it converts properly.
+ test_interpolation({
+ property: 'translate',
+ from: 'none',
+ to: '8px 80% 800px',
+ }, [
+ {at: -1, expect: '-8px -80% -800px'},
+ {at: 0, expect: '0px 0%'},
+ {at: 0.125, expect: '1px 10% 100px'},
+ {at: 0.875, expect: '7px 70% 700px'},
+ {at: 1, expect: '8px 80% 800px'},
+ {at: 2, expect: '16px 160% 1600px'}
+ ]);
+
+ // Test neutral keyframe; make sure it adds the underlying.
+ test_interpolation({
+ property: 'translate',
+ from: neutralKeyframe,
+ to: '20px',
+ }, [
+ {at: -1, expect: '0px'},
+ {at: 0, expect: '10px'},
+ {at: 0.25, expect: '12.5px'},
+ {at: 0.75, expect: '17.5px'},
+ {at: 1, expect: '20px'},
+ {at: 2, expect: '30px'},
+ ]);
+
+ // Test initial value; for 'scale' this is 'none'.
+ test_interpolation({
+ property: 'translate',
+ from: 'initial',
+ to: '200px 100px 200px',
+ }, [
+ {at: -1, expect: '-200px -100px -200px'},
+ {at: 0, expect: '0px'},
+ {at: 0.25, expect: '50px 25px 50px'},
+ {at: 0.75, expect: '150px 75px 150px'},
+ {at: 1, expect: '200px 100px 200px'},
+ {at: 2, expect: '400px 200px 400px'},
+ ]);
+
+ test_interpolation({
+ property: 'translate',
+ from: '200px 100px 400px',
+ to: 'initial',
+ }, [
+ {at: -1, expect: '400px 200px 800px'},
+ {at: 0, expect: '200px 100px 400px'},
+ {at: 0.25, expect: '150px 75px 300px'},
+ {at: 0.75, expect: '50px 25px 100px'},
+ {at: 1, expect: '0px'},
+ {at: 2, expect: '-200px -100px -400px'},
+ ]);
+
+
+ // Test unset value; for 'translate' this is 'none'.
+ test_interpolation({
+ property: 'translate',
+ from: 'unset',
+ to: '20px',
+ }, [
+ {at: -1, expect: '-20px'},
+ {at: 0, expect: '0px'},
+ {at: 0.25, expect: '5px'},
+ {at: 0.75, expect: '15px'},
+ {at: 1, expect: '20px'},
+ {at: 2, expect: '40px'},
+ ]);
+
+ // Test inherited value.
+ test_interpolation({
+ property: 'translate',
+ from: 'inherit',
+ to: '200px 100px 200px',
+ }, [
+ {at: -1, expect: '0px 300px 400px'},
+ {at: 0, expect: '100px 200px 300px'},
+ {at: 0.25, expect: '125px 175px 275px'},
+ {at: 0.75, expect: '175px 125px 225px'},
+ {at: 1, expect: '200px 100px 200px'},
+ {at: 2, expect: '300px 0px 100px'},
+ ]);
+
+ test_interpolation({
+ property: 'translate',
+ from: '200px 100px 200px',
+ to: 'inherit',
+ }, [
+ {at: -1, expect: '300px 0px 100px'},
+ {at: 0, expect: '200px 100px 200px'},
+ {at: 0.25, expect: '175px 125px 225px'},
+ {at: 0.75, expect: '125px 175px 275px'},
+ {at: 1, expect: '100px 200px 300px'},
+ {at: 2, expect: '0px 300px 400px'},
+ ]);
+
+ // Test combination of initial and inherit.
+ test_interpolation({
+ property: 'translate',
+ from: 'initial',
+ to: 'inherit',
+ }, [
+ {at: -1, expect: '-100px -200px -300px'},
+ {at: 0, expect: '0px'},
+ {at: 0.25, expect: '25px 50px 75px'},
+ {at: 0.75, expect: '75px 150px 225px'},
+ {at: 1, expect: '100px 200px 300px'},
+ {at: 2, expect: '200px 400px 600px'},
+ ]);
+
+ test_interpolation({
+ property: 'translate',
+ from: 'inherit',
+ to: 'initial',
+ }, [
+ {at: -1, expect: '200px 400px 600px'},
+ {at: 0, expect: '100px 200px 300px'},
+ {at: 0.25, expect: '75px 150px 225px'},
+ {at: 0.75, expect: '25px 50px 75px'},
+ {at: 1, expect: '0px'},
+ {at: 2, expect: '-100px -200px -300px'},
+ ]);
+ </script>
+ </body>
+</html>