rematrix
rematrix copied to clipboard
Decomposing a 3D matrix
First of all, great job with this library - the code is well-documented and clear. I was up late last night looking for a good library for manipulating CSS transforms, and was very happy to find this one!
I was wondering if you would be open to the following (small) features? I can definitely help with some of them, let me know if you are willing to accept PRs on any of these.
-
[x] b45c121 —
Rematrix.toString([...])for converting a matrix array to a CSS string. -
[x] 574e24e —
Rematrix.rotate(...)that defaults toRematrix.rotateZ(...)(re: CSS spec) -
[ ] ~Something that allows you to specify the unit for rotation, e.g.
Rematrix.rotate(0.5, 'rad')perhaps?~ (Moved to #2) -
[ ] Decomposition: probably a bigger feature ask, but a way to decompose a matrix and grab specific values such as
translateX,skewY,rotateZ,scaleY, etc. from it? -
[x] 0817c2d — Typescript definition file
Thanks so much for this library!
Thanks for the kind words @davidkpiano, I appreciate it!
"Decomposition - probably a bigger feature ask, but a way to decompose a matrix and grab specific values such as translateX, skewY, rotateZ, scaleY, etc. from it?"
Now this is 🔥 and strikes me as a very powerful addition to the API. Some cursory research has revealed it should be possible using inverse trigonometric functions.
I think this would add the most value for developers. If you’re up for it, I think it would make an outstanding contribution.
Can you show how you imagine the decomposition API, and what the method(s) return?
Sure, and for reference the W3C has a decomposing algorithm.
Potential APIs:
Calling decompose
// From an array (2d or 3d)
Rematrix.decompose([0.5, -1, 1, 0.5, 10, -20]);
Rematrix.decompose([1.4, 0, -1.4, 0, 0.3, 2, 0, 0, 0.7, 0, 0.7, 0, 11.3, 0, -11.3, 1]);
// From a string
Rematrix.decompose("matrix(0.5, -1, 1, 0.5, 10, -20)")
Rematrix.decompose("matrix3d(1.4, 0, -1.4, 0, 0.3, 2, 0, 0, 0.7, 0, 0.7, 0, 11.3, 0, -11.3, 1)")
Potential return value shapes
// hierarchical
{
translate: { x: 10, y: 0, z: 0 },
rotate: { x: 40, y: -25, z: 16 },
skew: { x: 0, y: 0 },
scale: { x: 1, y: 1, z: 1 },
perspective: 0
}
// as 3d vectors
{
translate: [10, 0, 0],
rotate: [40, -25, 16],
// ... etc.
}
// verbose
{
translateX: 10,
translateY: 0,
translateZ: 0,
// ... etc.
}
From the above, I prefer either { x, y, z } or [x, y, z] format. I think hierarchical is more explicit but vectors is probably more useful to use in a wider range of use cases that can just accept a vector, e.g., for translate.
Looks great. I think I’m learning towards the verbose model, because object keys then correspond to Rematrix method names (and facilitate an elegant compose() method)
var decomposed = Rematrix.decompose(...)
/**
* decomposed === {
* translateX: 10,
* translateY: 0,
* translateZ: 0,
* ... etc.
* }
*/
function compose(decomposed) {
return Object.keys(decomposed)
.map(key => Rematrix[key](decomposed[key]))
.reduce(Rematrix.multiply)
}
What’s your impression of this?
I think verbose is fine, you make a good point of it mapping directly to CSS property names and Rematrix methods.
Wow, I took a quick look at the W3C algorithm, and it was frightening. Also, I'm rather curious about the order of operations for composition. I'll keep looking.
The CSS string would be handy too. I thought format(array) was about it, but it seems it isn't.
I noticed the W3C CSS Transform Module document no longer included the 3D matrix decomposition sections. But thanks to WayBackMachine....
The pseudo code below is based upon the "unmatrix" method in "Graphics Gems II, edited by Jim Arvo", but modified to use Quaternions instead of Euler angles to avoid the problem of Gimbal Locks.
Input: matrix ; a 4x4 matrix
Output: translation ; a 3 component vector
scale ; a 3 component vector
skew ; skew factors XY,XZ,YZ represented as a 3 component vector
perspective ; a 4 component vector
quaternion ; a 4 component vector
Returns false if the matrix cannot be decomposed, true if it can
// Normalize the matrix.
if (matrix[3][3] == 0)
return false
for (i = 0; i < 4; i++)
for (j = 0; j < 4; j++)
matrix[i][j] /= matrix[3][3]
// perspectiveMatrix is used to solve for perspective, but it also provides
// an easy way to test for singularity of the upper 3x3 component.
perspectiveMatrix = matrix
for (i = 0; i < 3; i++)
perspectiveMatrix[i][3] = 0
perspectiveMatrix[3][3] = 1
if (determinant(perspectiveMatrix) == 0)
return false
// First, isolate perspective.
if (matrix[0][3] != 0 || matrix[1][3] != 0 || matrix[2][3] != 0)
// rightHandSide is the right hand side of the equation.
rightHandSide[0] = matrix[0][3]
rightHandSide[1] = matrix[1][3]
rightHandSide[2] = matrix[2][3]
rightHandSide[3] = matrix[3][3]
// Solve the equation by inverting perspectiveMatrix and multiplying
// rightHandSide by the inverse.
inversePerspectiveMatrix = inverse(perspectiveMatrix)
transposedInversePerspectiveMatrix = transposeMatrix4(inversePerspectiveMatrix)
perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix)
else
// No perspective.
perspective[0] = perspective[1] = perspective[2] = 0
perspective[3] = 1
// Next take care of translation
for (i = 0; i < 3; i++)
translate[i] = matrix[3][i]
// Now get scale and shear. 'row' is a 3 element array of 3 component vectors
for (i = 0; i < 3; i++)
row[i][0] = matrix[i][0]
row[i][1] = matrix[i][1]
row[i][2] = matrix[i][2]
// Compute X scale factor and normalize first row.
scale[0] = length(row[0])
row[0] = normalize(row[0])
// Compute XY shear factor and make 2nd row orthogonal to 1st.
skew[0] = dot(row[0], row[1])
row[1] = combine(row[1], row[0], 1.0, -skew[0])
// Now, compute Y scale and normalize 2nd row.
scale[1] = length(row[1])
row[1] = normalize(row[1])
skew[0] /= scale[1];
// Compute XZ and YZ shears, orthogonalize 3rd row
skew[1] = dot(row[0], row[2])
row[2] = combine(row[2], row[0], 1.0, -skew[1])
skew[2] = dot(row[1], row[2])
row[2] = combine(row[2], row[1], 1.0, -skew[2])
// Next, get Z scale and normalize 3rd row.
scale[2] = length(row[2])
row[2] = normalize(row[2])
skew[1] /= scale[2]
skew[2] /= scale[2]
// At this point, the matrix (in rows) is orthonormal.
// Check for a coordinate system flip. If the determinant
// is -1, then negate the matrix and the scaling factors.
pdum3 = cross(row[1], row[2])
if (dot(row[0], pdum3) < 0)
for (i = 0; i < 3; i++)
scale[i] *= -1;
row[i][0] *= -1
row[i][1] *= -1
row[i][2] *= -1
// Now, get the rotations out
quaternion[0] = 0.5 * sqrt(max(1 + row[0][0] - row[1][1] - row[2][2], 0))
quaternion[1] = 0.5 * sqrt(max(1 - row[0][0] + row[1][1] - row[2][2], 0))
quaternion[2] = 0.5 * sqrt(max(1 - row[0][0] - row[1][1] + row[2][2], 0))
quaternion[3] = 0.5 * sqrt(max(1 + row[0][0] + row[1][1] + row[2][2], 0))
if (row[2][1] > row[1][2])
quaternion[0] = -quaternion[0]
if (row[0][2] > row[2][0])
quaternion[1] = -quaternion[1]
if (row[1][0] > row[0][1])
quaternion[2] = -quaternion[2]
return true
☠️
Example of the W3 implementation: https://github.com/facebook/react-native/blob/master/Libraries/Utilities/MatrixMath.js#L572
But I think the Facebook implementation has bugs (line duplicates? double check quaternions convetation) and also it's not optimized. There are a lot of unrequired arrays spawns.
Also, probably, perspective calc can be skipped?
And algorithm for the 3d matrix decomposition is moved here: https://www.w3.org/TR/css-transforms-2/