<!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>
