Fix matrix3d transform under page zoom

The math for adjusting a matrix3d transform when the page is zoomed is incorrect in at least 3 different places.

There was a previous attempt at fixing this but they did it the same way as the 2d matrix case and forgot about the translateZ component (ceddaab902432728b12a17fcbf2c42283c7dfe68). But it would've been incorrect even if they did take translateZ into account.

The real problem is that the last column can't be interpeted as the translation at all if the matrix is non-affine!

To get the general correction for matrix3d, intuitively, we want to first scale down to the original units, do the transform, then scale back up. So checking with octave/matlab it should actually be:

    octave:1> pkg load symbolic;
    octave:2> A = sym('a', [3, 3]); B = sym('b', [3, 1]); C = sym('c', [1, 3]); D = sym('d', [1, 1]); s = sym('s');
    octave:3> M = [A, B; C D]
    M = (sym 4×4 matrix)

      ⎡a₁₁  a₁₂  a₁₃  b₁₁⎤
      ⎢                  ⎥
      ⎢a₂₁  a₂₂  a₂₃  b₂₁⎥
      ⎢                  ⎥
      ⎢a₃₁  a₃₂  a₃₃  b₃₁⎥
      ⎢                  ⎥
      ⎣c₁₁  c₁₂  c₁₃  d₁₁⎦

    octave:4> scaleDown = [[eye(3) / s, zeros(3, 1)]; [zeros(1, 3), 1]]
    scaleDown = (sym 4×4 matrix)

      ⎡1         ⎤
      ⎢─  0  0  0⎥
      ⎢s         ⎥
      ⎢          ⎥
      ⎢   1      ⎥
      ⎢0  ─  0  0⎥
      ⎢   s      ⎥
      ⎢          ⎥
      ⎢      1   ⎥
      ⎢0  0  ─  0⎥
      ⎢      s   ⎥
      ⎢          ⎥
      ⎣0  0  0  1⎦

    octave:5> scaleUp = [[s * eye(3), zeros(3, 1)]; [zeros(1, 3), 1]]
    scaleUp = (sym 4×4 matrix)

      ⎡s  0  0  0⎤
      ⎢          ⎥
      ⎢0  s  0  0⎥
      ⎢          ⎥
      ⎢0  0  s  0⎥
      ⎢          ⎥
      ⎣0  0  0  1⎦

    octave:6> scaleUp * M * scaleDown
    ans = (sym 4×4 matrix)

      ⎡a₁₁  a₁₂  a₁₃  b₁₁⋅s⎤
      ⎢                    ⎥
      ⎢a₂₁  a₂₂  a₂₃  b₂₁⋅s⎥
      ⎢                    ⎥
      ⎢a₃₁  a₃₂  a₃₃  b₃₁⋅s⎥
      ⎢                    ⎥
      ⎢c₁₁  c₁₂  c₁₃       ⎥
      ⎢───  ───  ───   d₁₁ ⎥
      ⎣ s    s    s        ⎦

So in the above matrix, if it is affine (which is exactly when the `c` entries are 0 and `d` is 1) you can get away with just scaling the last column interpreting `b` as the translation.

But for a general matrix3d you still need a few more divides on the bottom row!

Sidenote: The perspective transform is the only other CSS transform that is non-affine and it gets the zoom division implicitly when the parameter is scaled up since it's in the form of:

  ⎡1  0   0   0⎤
  ⎢            ⎥
  ⎢0  1   0   0⎥
  ⎢            ⎥
  ⎢0  0   1   0⎥
  ⎢            ⎥
  ⎢      -1    ⎥
  ⎢0  0  ───  1⎥
  ⎣       p    ⎦

The reported issue is solved by fixing TransformBuilder as described above. But the same type of bug also appeared in getComputedStyle and animations with matrix3d. I also fixed them in this CL but a longer term solution should probably refactor all them (maybe into TransformOperation::zoom) so that this tricky bit of logic isn't duplicated in so many places.

BUG=242685
R=pdr,alancutter

Review-Url: https://codereview.chromium.org/2482753002
Cr-Commit-Position: refs/heads/master@{#431208}
diff --git a/AUTHORS b/AUTHORS
index 7302863..479f949 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -213,6 +213,7 @@
 Francois Kritzinger <francoisk777@gmail.com>
 Francois Rauch <leopardb@gmail.com>
 Frankie Dintino <fdintino@theatlantic.com>
+Franklin Ta <fta2012@gmail.com>
 Frédéric Jacob <frederic.jacob.78@gmail.com>
 Frédéric Wang <fred.wang@free.fr>
 Gaetano Mendola <mendola@gmail.com>
diff --git a/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation-expected.html b/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation-expected.html
index 94ea8e7..95c25c6c 100644
--- a/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation-expected.html
+++ b/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation-expected.html
@@ -25,6 +25,11 @@
     '1, 0, 0, 0, ' +
     '0, 0, 1, 0, ' +
     '10, 10, 10, 1)',
+  'matrix3d(' +
+    '0.707106781186548, 0.000000000000000, -0.707106781186547, 0.003535533905933,' +
+    '0.000000000000000, 1.000000000000000, 0.000000000000000, 0.000000000000000,' +
+    '0.707106781186547, 0.000000000000000, 0.707106781186548, -0.003535533905933,' +
+    '0.000000000000000, 0.000000000000000, 0.000000000000000, 1.000000000000000)', // Equivalent to perspective(200px) rotateY(45deg)
   'perspective(200px) rotateY(45deg)',
   'none', // Composited animations fail to zoom the last expectation correctly. ):
 ].forEach(transform => {
diff --git a/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation.html b/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation.html
index 88527db..1767b259 100644
--- a/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation.html
+++ b/third_party/WebKit/LayoutTests/animations/zoom-responsive-transform-animation.html
@@ -23,6 +23,11 @@
     '1, 0, 0, 0, ' +
     '0, 0, 1, 0, ' +
     '10, 10, 10, 1)',
+  'matrix3d(' +
+    '0.707106781186548, 0.000000000000000, -0.707106781186547, 0.003535533905933,' +
+    '0.000000000000000, 1.000000000000000, 0.000000000000000, 0.000000000000000,' +
+    '0.707106781186547, 0.000000000000000, 0.707106781186548, -0.003535533905933,' +
+    '0.000000000000000, 0.000000000000000, 0.000000000000000, 1.000000000000000)',
   'perspective(200px) rotateY(45deg)',
   'none',
 ].forEach(transform => {
diff --git a/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/computed-style-with-zoom-expected.txt b/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/computed-style-with-zoom-expected.txt
index b304fa9..67663ae 100644
--- a/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/computed-style-with-zoom-expected.txt
+++ b/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/computed-style-with-zoom-expected.txt
@@ -18,6 +18,7 @@
 PASS -webkit-text-stroke-width, value: "20px"
 PASS -webkit-transform, value: "matrix(1, 0, 0, 1, 20, 20)"
 PASS -webkit-transform, value: "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 20, 20, 20, 1)"
+PASS -webkit-transform, value: "matrix3d(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)"
 PASS -webkit-transform-origin, value: "20px 20px"
 PASS -webkit-transform-origin, value: "-20px -20px"
 PASS background-position-x, value: "20px"
diff --git a/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/script-tests/computed-style-with-zoom.js b/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/script-tests/computed-style-with-zoom.js
index e9584f8..ac26059 100644
--- a/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/script-tests/computed-style-with-zoom.js
+++ b/third_party/WebKit/LayoutTests/fast/css/getComputedStyle/script-tests/computed-style-with-zoom.js
@@ -49,7 +49,7 @@
     ['-webkit-mask-position-y', '20px', '-20px'],
     ['-webkit-perspective-origin', '20px 20px'],
     ['-webkit-text-stroke-width'],
-    ['-webkit-transform', 'translate(20px, 20px)', 'translate3d(20px, 20px, 20px)'],
+    ['-webkit-transform', 'translate(20px, 20px)', 'translate3d(20px, 20px, 20px)', 'matrix3d(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)'],
     ['-webkit-transform-origin', '20px 20px', '-20px -20px'],
     ['background-position-x', '20px', '-20px'],
     ['background-position-y', '20px', '-20px'],
diff --git a/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom-expected.png b/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom-expected.png
index d974efb..4ea755b 100644
--- a/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom-expected.png
+++ b/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom-expected.png
Binary files differ
diff --git a/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom.html b/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom.html
index ccf106d..ffbac59 100644
--- a/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom.html
+++ b/third_party/WebKit/LayoutTests/transforms/3d/general/cssmatrix-3d-zoom.html
@@ -17,7 +17,7 @@
 }
 
 .translate {
-    transform: translate(100px, 50px);
+    transform: perspective(200px) translate3d(100px, 50px, 25px) rotateY(45deg);
     background-color: red;
 }
 
@@ -41,7 +41,8 @@
         var y2 = element2.getBoundingClientRect().top;
 
         var resultString = '';
-        if (x1 == x2 && y1 == y2) {
+        var tolerance = 1e-2;
+        if (Math.abs(x1 - x2) < tolerance && Math.abs(y1 - y2) < tolerance) {
             resultString += "PASS - Element " + id1 + " and Element " + id2 + " had identical positions";
         } else {
             resultString += "FAIL - Element " + id1 + " and Element " + id2 + " had different positions";
@@ -60,7 +61,7 @@
 <div id='b' class="box matrix"></div>
 
 <script>
-    document.getElementById('b').style.webkitTransform = new WebKitCSSMatrix('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 100, 50, 0, 1)');
+    document.getElementById('b').style.webkitTransform = new WebKitCSSMatrix('matrix3d(0.707107, 0, -0.707107, 0.00353553, 0, 1, 0, 0, 0.707107, 0, 0.707107, -0.00353553, 100, 50, 25, 0.875)');
 </script>
 </body>
 </html>
diff --git a/third_party/WebKit/Source/core/css/ComputedStyleCSSValueMapping.cpp b/third_party/WebKit/Source/core/css/ComputedStyleCSSValueMapping.cpp
index 8245fda..acbc03e 100644
--- a/third_party/WebKit/Source/core/css/ComputedStyleCSSValueMapping.cpp
+++ b/third_party/WebKit/Source/core/css/ComputedStyleCSSValueMapping.cpp
@@ -90,13 +90,6 @@
   return zoomAdjustedPixelValue(length.value(), style);
 }
 
-inline static CSSPrimitiveValue* zoomAdjustedNumberValue(
-    double value,
-    const ComputedStyle& style) {
-  return CSSPrimitiveValue::create(value / style.effectiveZoom(),
-                                   CSSPrimitiveValue::UnitType::Number);
-}
-
 static CSSValue* zoomAdjustedPixelValueForLength(const Length& length,
                                                  const ComputedStyle& style) {
   if (length.isFixed())
@@ -1352,10 +1345,10 @@
   return list;
 }
 
-static CSSFunctionValue* valueForMatrixTransform(
-    const TransformationMatrix& transform,
-    const ComputedStyle& style) {
+static CSSFunctionValue* valueForMatrixTransform(TransformationMatrix transform,
+                                                 const ComputedStyle& style) {
   CSSFunctionValue* transformValue = nullptr;
+  transform.zoom(1 / style.effectiveZoom());
   if (transform.isAffine()) {
     transformValue = CSSFunctionValue::create(CSSValueMatrix);
 
@@ -1367,8 +1360,10 @@
         transform.c(), CSSPrimitiveValue::UnitType::Number));
     transformValue->append(*CSSPrimitiveValue::create(
         transform.d(), CSSPrimitiveValue::UnitType::Number));
-    transformValue->append(*zoomAdjustedNumberValue(transform.e(), style));
-    transformValue->append(*zoomAdjustedNumberValue(transform.f(), style));
+    transformValue->append(*CSSPrimitiveValue::create(
+        transform.e(), CSSPrimitiveValue::UnitType::Number));
+    transformValue->append(*CSSPrimitiveValue::create(
+        transform.f(), CSSPrimitiveValue::UnitType::Number));
   } else {
     transformValue = CSSFunctionValue::create(CSSValueMatrix3d);
 
@@ -1399,9 +1394,12 @@
     transformValue->append(*CSSPrimitiveValue::create(
         transform.m34(), CSSPrimitiveValue::UnitType::Number));
 
-    transformValue->append(*zoomAdjustedNumberValue(transform.m41(), style));
-    transformValue->append(*zoomAdjustedNumberValue(transform.m42(), style));
-    transformValue->append(*zoomAdjustedNumberValue(transform.m43(), style));
+    transformValue->append(*CSSPrimitiveValue::create(
+        transform.m41(), CSSPrimitiveValue::UnitType::Number));
+    transformValue->append(*CSSPrimitiveValue::create(
+        transform.m42(), CSSPrimitiveValue::UnitType::Number));
+    transformValue->append(*CSSPrimitiveValue::create(
+        transform.m43(), CSSPrimitiveValue::UnitType::Number));
     transformValue->append(*CSSPrimitiveValue::create(
         transform.m44(), CSSPrimitiveValue::UnitType::Number));
   }
diff --git a/third_party/WebKit/Source/core/css/resolver/TransformBuilder.cpp b/third_party/WebKit/Source/core/css/resolver/TransformBuilder.cpp
index 0c00516..3bea8d9 100644
--- a/third_party/WebKit/Source/core/css/resolver/TransformBuilder.cpp
+++ b/third_party/WebKit/Source/core/css/resolver/TransformBuilder.cpp
@@ -305,12 +305,11 @@
             toCSSPrimitiveValue(transformValue->item(9)).getDoubleValue(),
             toCSSPrimitiveValue(transformValue->item(10)).getDoubleValue(),
             toCSSPrimitiveValue(transformValue->item(11)).getDoubleValue(),
-            zoomFactor *
-                toCSSPrimitiveValue(transformValue->item(12)).getDoubleValue(),
-            zoomFactor *
-                toCSSPrimitiveValue(transformValue->item(13)).getDoubleValue(),
+            toCSSPrimitiveValue(transformValue->item(12)).getDoubleValue(),
+            toCSSPrimitiveValue(transformValue->item(13)).getDoubleValue(),
             toCSSPrimitiveValue(transformValue->item(14)).getDoubleValue(),
             toCSSPrimitiveValue(transformValue->item(15)).getDoubleValue());
+        matrix.zoom(zoomFactor);
         operations.operations().append(
             Matrix3DTransformOperation::create(matrix));
         break;
diff --git a/third_party/WebKit/Source/platform/transforms/Matrix3DTransformOperation.cpp b/third_party/WebKit/Source/platform/transforms/Matrix3DTransformOperation.cpp
index fb1149f..bc5e312 100644
--- a/third_party/WebKit/Source/platform/transforms/Matrix3DTransformOperation.cpp
+++ b/third_party/WebKit/Source/platform/transforms/Matrix3DTransformOperation.cpp
@@ -54,8 +54,7 @@
 
 PassRefPtr<TransformOperation> Matrix3DTransformOperation::zoom(double factor) {
   TransformationMatrix result = m_matrix;
-  result.setM41(result.m41() * factor);
-  result.setM42(result.m42() * factor);
+  result.zoom(factor);
   return create(result);
 }
 
diff --git a/third_party/WebKit/Source/platform/transforms/TransformOperationsTest.cpp b/third_party/WebKit/Source/platform/transforms/TransformOperationsTest.cpp
index f1f6245..cf6bdd1 100644
--- a/third_party/WebKit/Source/platform/transforms/TransformOperationsTest.cpp
+++ b/third_party/WebKit/Source/platform/transforms/TransformOperationsTest.cpp
@@ -508,4 +508,36 @@
                       FloatBox(-7, -3, 2, 15, 23, 20), bounds);
 }
 
+TEST(TransformOperationsTest, ZoomTest) {
+  double zoomFactor = 1.25;
+
+  FloatPoint3D originalPoint(2, 3, 4);
+
+  TransformOperations ops;
+  ops.operations().append(TranslateTransformOperation::create(
+      Length(1, Fixed), Length(2, Fixed), 3, TransformOperation::Translate3D));
+  ops.operations().append(PerspectiveTransformOperation::create(1234));
+  ops.operations().append(
+      Matrix3DTransformOperation::create(TransformationMatrix(
+          1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)));
+
+  // Apply unzoomed ops to unzoomed units, then zoom in
+  FloatPoint3D unzoomedPoint = originalPoint;
+  TransformOperations unzoomedOps = ops;
+  TransformationMatrix unzoomedMatrix;
+  ops.apply(FloatSize(0, 0), unzoomedMatrix);
+  FloatPoint3D result1 = unzoomedMatrix.mapPoint(unzoomedPoint);
+  result1.scale(zoomFactor, zoomFactor, zoomFactor);
+
+  // Apply zoomed ops to zoomed units
+  FloatPoint3D zoomedPoint = originalPoint;
+  zoomedPoint.scale(zoomFactor, zoomFactor, zoomFactor);
+  TransformOperations zoomedOps = ops.zoom(zoomFactor);
+  TransformationMatrix zoomedMatrix;
+  zoomedOps.apply(FloatSize(0, 0), zoomedMatrix);
+  FloatPoint3D result2 = zoomedMatrix.mapPoint(zoomedPoint);
+
+  EXPECT_EQ(result1, result2);
+}
+
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/transforms/TransformationMatrix.cpp b/third_party/WebKit/Source/platform/transforms/TransformationMatrix.cpp
index f0cb81c..6b7411b 100644
--- a/third_party/WebKit/Source/platform/transforms/TransformationMatrix.cpp
+++ b/third_party/WebKit/Source/platform/transforms/TransformationMatrix.cpp
@@ -1266,6 +1266,16 @@
   return *this;
 }
 
+TransformationMatrix& TransformationMatrix::zoom(double zoomFactor) {
+  m_matrix[0][3] /= zoomFactor;
+  m_matrix[1][3] /= zoomFactor;
+  m_matrix[2][3] /= zoomFactor;
+  m_matrix[3][0] *= zoomFactor;
+  m_matrix[3][1] *= zoomFactor;
+  m_matrix[3][2] *= zoomFactor;
+  return *this;
+}
+
 // Calculates *this = *this * mat.
 // Note: A * B means that the transforms represented by A happen first, and
 // then the transforms represented by B. That is, the matrix A * B corresponds
diff --git a/third_party/WebKit/Source/platform/transforms/TransformationMatrix.h b/third_party/WebKit/Source/platform/transforms/TransformationMatrix.h
index f9ae7d8..f2bc7d9 100644
--- a/third_party/WebKit/Source/platform/transforms/TransformationMatrix.h
+++ b/third_party/WebKit/Source/platform/transforms/TransformationMatrix.h
@@ -337,6 +337,16 @@
     return applyTransformOrigin(origin.x(), origin.y(), origin.z());
   }
 
+  // Changes the transform to:
+  //
+  //     scale3d(z, z, z) * mat * scale3d(1/z, 1/z, 1/z)
+  //
+  // Useful for mapping zoomed points to their zoomed transformed result:
+  //
+  //     new_mat * (scale3d(z, z, z) * x) == scale3d(z, z, z) * (mat * x)
+  //
+  TransformationMatrix& zoom(double zoomFactor);
+
   bool isInvertible() const;
 
   // This method returns the identity matrix if it is not invertible.