w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.48k stars 659 forks source link

[css-transforms] Inconsistencies in 2D and 3D matrix decomposition #3713

Open kevers-google opened 5 years ago

kevers-google commented 5 years ago

2D spec: https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix 3D spec: https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix

A minor tweak to a transformation matrix that alters the transform from 2D to 3D, or vice versa, should not radically alter the interpolation path.

In the 2D case, the decomposed matrix has a total of 9 parameters: 2 translate, 2 scale, an angle and a 2x2 matrix that corresponds to the skew. This feels over-specified as the skew should be expressible with at most 2 parameters.

Here is an alternative, which is more consistent with the 3D matrix decomposition:

Input:  
    matrix      ; a 4x4 matrix
Output: 
    translation ; a 2 component vector
    scale       ; a 2 component vector
    skew        ; X skew
    angle       ; rotation
Returns false if the matrix cannot be decomposed, true if it can

m11 = matrix[0][0]
m12 = matrix[0][1]
m21 = matrix[1][0]
m22 = matrix[1][1]

determinant = m11 * m22 - m12 * m21;
if (determinant == 0)
    return false;

translate[0] = matrix[3][0]
translate[1] = matrix[3][1]

scale[0] = 1;
scale[1] = 1;
if (determinant < 0)
    // If the determinant is negative, we need to flip either the x or y scale.
    // Flipping both is equivalent to rotating by 180 degrees.
    if (m11 < m22)
        scale[0] = -1
    else
        scale[1] = -1

scale[0] *= sqrt(m11 * m11 + m12 * m12)
m11 /= scale[0]
m12 /= scale[0]

scaledShear = m11 * m21 + m12 * m22
m21 -= m11 * scaledShear
m22 -= m12 * scaledShear

scale[1] *= sqrt(m21 * m21 + m22 * m22)
m21 /= scale[1]
m22 /= scale[1]
skew = scaledShear / scale[1]
angle = atan2(m12, m11)

The inconsistency in handling negative determinants is files as https://github.com/w3c/csswg-drafts/issues/3712.

The 3D counterpart to this 2D decomposition is:

decomp3d.scale = [decomp2d.scale[0], decomp2d.scale[1], 1] decomp3d.translate = [decomp2d.translate[0], decomp2d.translate[1], 0] decomp3d.skew = [decomp2d.skew, 0, 0] decomp3d.perspective = [0, 0, 0, 1] decomp3d.quaternion = [0, 0, sin(decomp2d.angle/2), cos(decomp2d.angle/2)]

Having this simple mapping facilitates blending a 2D and 3D transformation matrix.

birtles commented 5 years ago

I'm pretty sure this is a dupe of #3230.

birtles commented 5 years ago

I've been working through this trying to fix inconsistencies in Gecko but I'm having trouble understanding how this produces consistent results for 2D/3D for input such as matrix(1, 0, 0, -1, 0, 0).

As far as I can tell, the 3D decomposition would yield a quaternion of 0, 1, 0, 0 and debugging Gecko and Chrome I see they both give that.

However, using the algorithm above we have:

decomp3d.quaternion = [0, 0, sin(decomp2d.angle/2), cos(decomp2d.angle/2)]

Which is clearly never going to give 0, 1, 0, 0.

Instead, applying the above logic in Gecko I get 0, 0, 0, 1.

@kevers-google Is there something I'm missing here?

kevers-google commented 5 years ago

The 2D algorithm flips one scale factor if the determinant is negative whereas the specced 3D algorithm flips all scale factors. Both have the impact of making the matrix determinant positive -- though in the 3D case, this will lead to rotations in addition to the scaling as well as create a throbbing effect. If in the 3D algorithm a single axis was selected for scale inversion to address the negative determinant in a consistent manner to the 2D algorithm, the 2D and 3D results should align.