Skip to content

Instantly share code, notes, and snippets.

@BriSeven
Last active May 23, 2021 15:21
Show Gist options
  • Save BriSeven/9d217e0375de055d563b9a0b758d4ae6 to your computer and use it in GitHub Desktop.
Save BriSeven/9d217e0375de055d563b9a0b758d4ae6 to your computer and use it in GitHub Desktop.
Decompose a 2D transform matrix into [rotate scale rotate translate]
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>&#x2212;</mo><mn>"+Math.abs(x)+"</mn></mrow>";
return "<mn>"+x+"</mn>";
}
function trig(func, arg)
{
// MathML trigonometric function.
return "<mrow><mi>"+func+"</mi><mo>&#x2061;</mo><mrow><mo>(</mo>"+
mn(arg / Math.PI)+"<mi>&#x3c0;</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();
}
@BriSeven
Copy link
Author

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.

@wcandillon
Copy link

@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