Last active
May 23, 2021 15:21
-
-
Save BriSeven/9d217e0375de055d563b9a0b758d4ae6 to your computer and use it in GitHub Desktop.
Decompose a 2D transform matrix into [rotate scale rotate translate]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function decomposeMatrix(m) { | |
var t,r,s,k,E,F,G,H,Q,R,sx,sy,a1,a2,theta,phi,sqrt=Math.sqrt,atan2=Math.atan2; | |
// http://math.stackexchange.com/questions/861674/decompose-a-2d-arbitrary-transform-into-only-scaling-and-rotation | |
// | |
// It works wonderfully! Thanks. | |
// The input matrix is transposed though, | |
// so let me spell the solution out. | |
E=(m[0]+m[3])/2 | |
F=(m[0]-m[3])/2 | |
G=(m[2]+m[1])/2 | |
H=(m[2]-m[1])/2 | |
Q=sqrt(E*E+H*H); | |
R=sqrt(F*F+G*G); | |
sx=Q+R; | |
sy=Q-R; | |
a1=atan2(G,F); | |
a2=atan2(H,E); | |
theta=(a2-a1)/2; | |
phi=(a2+a1)/2; | |
// The requested parameters are then theta, | |
// sx, sy, phi, | |
// i.e. rotate by theta, | |
k=-theta*180/Math.PI; | |
// scale by sx,sy, | |
s=[sx,sy]; | |
// rotate by phi. | |
r=-phi*180/Math.PI; | |
//No division by zero or sqrt(negative) hazard. Excellent. | |
t=[m[4],m[5]]; | |
return {translate:t,rotate:r,scale:s,skew:k}; | |
} | |
decomposed_toString = function(tr) { | |
return [ | |
"translate(" + tr.translate.join(",") + ")", | |
"rotate(" + tr.skew + ")", | |
"scale(" + tr.scale.join(",") + ")", | |
"rotate(" + tr.rotate + ")", | |
].join(" "); | |
}; | |
// http://frederic-wang.fr/decomposition-of-2d-transform-matrices.html | |
var TransformName = null; | |
var CSSdecomposition, SVGdecomposition, MatrixDecomposition; | |
function initTransformName() | |
{ | |
// Initialize TransformName with the appropriate CSS property name. | |
if (TransformName) return true; | |
var test = document.getElementById("test"); | |
var nameList = ["transform", "-moz-transform", "-webkit-transform", | |
"-o-transform"]; | |
for (var i in nameList) { | |
TransformName = nameList[i]; | |
if (getComputedStyle(test)[TransformName] === "none") return true; | |
} | |
return false; | |
} | |
function radToDeg(a) | |
{ | |
// Radian to degree. | |
return 180 * a / Math.PI; | |
} | |
function mn(x) | |
{ | |
// MathML Number. | |
if (x < 0) | |
return "<mrow><mo>−</mo><mn>"+Math.abs(x)+"</mn></mrow>"; | |
return "<mn>"+x+"</mn>"; | |
} | |
function trig(func, arg) | |
{ | |
// MathML trigonometric function. | |
return "<mrow><mi>"+func+"</mi><mo>⁡</mo><mrow><mo>(</mo>"+ | |
mn(arg / Math.PI)+"<mi>π</mi><mo>)</mo></mrow></mrow>"; | |
} | |
function matrix(a, b, c, d, e, f) | |
{ | |
// MathML 2D matrix. | |
return "<mrow><mo>(</mo><mtable><mtr><mtd>"+a+"</mtd><mtd>"+c+"</mtd><mtd>"+e+"</mtd></mtr><mtr><mtd>"+b+"</mtd><mtd>"+d+"</mtd><mtd>"+f+"</mtd></mtr><mtr><mtd><mn>0</mn></mtd><mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd></mtr></mtable><mo>)</mo></mrow>" | |
} | |
function newTranslate(tx, ty) | |
{ | |
// Add a translate. | |
if (tx == 0 && ty == 0) return; | |
MatrixDecomposition += | |
matrix(mn(1), mn(0), mn(0), mn(1), mn(tx), mn(ty)); | |
if (ty == 0) { | |
SVGdecomposition += "translate(" + tx + ") "; | |
CSSdecomposition += "translate(" + tx + "px) "; | |
} else { | |
SVGdecomposition += "translate(" + tx + ", " + ty + ") "; | |
CSSdecomposition += "translate(" + tx + "px, " + ty + "px) "; | |
} | |
} | |
function newScale(sx, sy) | |
{ | |
// Add a scale. | |
if (sx == 1 && sy == 1) return; | |
MatrixDecomposition += | |
matrix(mn(sx), mn(0), | |
mn(0), mn(sy), mn(0), mn(0)); | |
var s = "scale(" + sx + (sx == sy ? "" : "," + sy) + ") "; | |
SVGdecomposition += s; | |
CSSdecomposition += s; | |
} | |
function newRotate(a) | |
{ | |
// Add a rotation. | |
if (a == 0) return ""; | |
MatrixDecomposition += | |
matrix(trig("cos", a), trig("sin", a), | |
trig("sin", -a), trig("cos", a), mn(0), mn(0)); | |
a = radToDeg(a); | |
SVGdecomposition += "rotate(" + a + ") "; | |
CSSdecomposition += "rotate(" + a + "deg) "; | |
} | |
function newSkewX(a) | |
{ | |
// Add a skewX. | |
if (a == 0) return; | |
MatrixDecomposition += | |
matrix(mn(1), mn(0), | |
trig("tan", a), mn(1), mn(0), mn(0), mn(0)); | |
a = radToDeg(a); | |
SVGdecomposition += "skewX(" + a + ") "; | |
CSSdecomposition += "skewX(" + a + "deg) "; | |
} | |
function newSkewY(a) | |
{ | |
// Add a skewY. | |
if (a == 0) return; | |
MatrixDecomposition += | |
matrix(mn(1), trig("tan", a), | |
mn(0), mn(1), mn(0), mn(0), mn(0)); | |
a = radToDeg(a); | |
SVGdecomposition += "skewY(" + a + ") "; | |
CSSdecomposition += "skewY(" + a + "deg) "; | |
} | |
function decompose() | |
{ | |
// Verify if a CSS transform is available. | |
if (!initTransformName()) | |
throw "Your browser does not support CSS transforms." | |
// Apply the transform specified by the user. | |
var cssRect1 = document.getElementById("cssRect1"); | |
var CSS2Dtransform = document.getElementById("CSS2Dtransform").value; | |
cssRect1.style[TransformName] = "none"; | |
cssRect1.style[TransformName] = CSS2Dtransform; | |
// Get the matrix computed by the rendering engine. | |
var CSS2Dmatrix = getComputedStyle(cssRect1)[TransformName]; | |
var regexp = /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/; | |
var match = regexp.exec(CSS2Dmatrix); | |
if (match === null) | |
throw "Syntax Error. Please enter a valid CSS 2D transform." | |
var a = parseFloat(match[1]); | |
var b = parseFloat(match[2]); | |
var c = parseFloat(match[3]); | |
var d = parseFloat(match[4]); | |
var e = parseFloat(match[5]); | |
var f = parseFloat(match[6]); | |
document.getElementById("CSS2Dmatrix").innerHTML = CSS2Dmatrix; | |
document.getElementById("CSS2DmatrixMathML").innerHTML = | |
"<math display='block'>" + | |
matrix(mn(a), mn(b), mn(c), mn(d), mn(e), mn(f)) + "</math>" | |
// Apply the decomposition algorithm. | |
CSSdecomposition = ""; SVGdecomposition = ""; MatrixDecomposition = ""; | |
newTranslate(e, f); | |
var Delta = a * d - b * c; | |
if (document.getElementById("decompo").value == "QR-like") { | |
// Apply the QR-like decomposition. | |
if (a != 0 || b != 0) { | |
var r = Math.sqrt(a*a+b*b); | |
newRotate(b > 0 ? Math.acos(a/r) : -Math.acos(a/r)); | |
newScale(r, Delta/r); | |
newSkewX(Math.atan((a*c+b*d)/(r*r))); | |
} else if (c != 0 || d != 0) { | |
var s = Math.sqrt(c*c+d*d); | |
newRotate(Math.PI/2 - (d > 0 ? Math.acos(-c/s) : -Math.acos(c/s))); | |
newScale(Delta/s, s); | |
newSkewY(Math.atan((a*c+b*d)/(s*s))); | |
} else { // a = b = c = d = 0 | |
newScale(0, 0); | |
} | |
} else { | |
// Apply the LU-like decomposition. | |
if (a != 0) { | |
newSkewY(Math.atan(b/a)); | |
newScale(a, Delta/a); | |
newSkewX(Math.atan(c/a)); | |
} else if (b != 0) { | |
newRotate(Math.PI / 2); | |
newScale(b, Delta/b); | |
newSkewX(Math.atan(d/b)); | |
} else { // a = b = 0 | |
newScale(c, d); | |
newSkewX(Math.PI/4); | |
newScale(0, 1); | |
} | |
} | |
// Display something if the transform is the identity. | |
if (MatrixDecomposition === "") { | |
MatrixDecomposition = | |
matrix(mn(1), mn(0), mn(0), mn(1), mn(0), mn(0)); | |
CSSdecomposition = SVGdecomposition = "scale(1)"; | |
} | |
// Display the result. | |
document.getElementById("SVGdecomposition").innerHTML = | |
SVGdecomposition; | |
document.getElementById("CSSdecomposition").innerHTML = | |
CSSdecomposition; | |
document.getElementById("MatrixDecomposition").innerHTML = | |
"<math display='block'>"+MatrixDecomposition+"</math>" | |
// Apply the (decomposed) transformation to the SVG and CSS elements. | |
document.getElementById("svgRect"). | |
setAttribute("transform", SVGdecomposition); | |
cssRect2.style[TransformName] = CSSdecomposition; | |
} | |
function run() | |
{ | |
var error = document.getElementById("error"); | |
try { | |
error.innerHTML = ""; | |
decompose(); | |
} catch (e) { | |
error.innerHTML = e; | |
} | |
} | |
function update(v) | |
{ | |
if (v === "") return; | |
document.getElementById("CSS2Dtransform").value = v; | |
run(); | |
} |
it’s two different functions from two different origins copied and pasted together. both are here because combining into a matrix is a lossy operation, with multiple possible base transforms a matrix might represent. i found these two and collected them here because they both worked when I tried them, but gave different results.
they worked when i tried them years ago. they each might need their input in a slightly different format though. as for getting them to work in react? no idea about that. they work in javascript.
@breton Thank you for answering, really appreciate it. I can confirm that it works great (I tried the first solution). What a fun mathematical tool :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is this gist really working? I'm trying to implement it using React Native Reanimated but I'm not successful with it. Maybe a bug in my code.