From 3ee46ffc5c85784a34f99c9d56e8b1e04c038189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 14 Feb 2016 10:59:57 +0100 Subject: [PATCH] Matrix: Switch to a better implementation of #decompose() This now also correctly handles skewing in SVG export. --- src/basic/Matrix.js | 61 ++++++++++++++++++++++---------------------- src/item/Item.js | 6 ++--- src/svg/SvgExport.js | 13 +++++++--- test/tests/Matrix.js | 20 +++++++-------- 4 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/basic/Matrix.js b/src/basic/Matrix.js index d3453ab9..b9ea1828 100644 --- a/src/basic/Matrix.js +++ b/src/basic/Matrix.js @@ -652,40 +652,39 @@ var Matrix = Base.extend(/** @lends Matrix# */{ */ decompose: function() { // http://dev.w3.org/csswg/css3-2d-transforms/#matrix-decomposition - // http://stackoverflow.com/questions/4361242/ + // http://www.maths-informatique-jeux.com/blog/frederic/?post/2013/12/01/Decomposition-of-2D-transform-matrices // https://github.com/wisec/DOMinator/blob/master/layout/style/nsStyleAnimation.cpp#L946 - var a = this._a, b = this._c, c = this._b, d = this._d; - if (Numerical.isZero(a * d - b * c)) - return null; - - var scaleX = Math.sqrt(a * a + b * b); - a /= scaleX; - b /= scaleX; - - var shear = a * c + b * d; - c -= a * shear; - d -= b * shear; - - var scaleY = Math.sqrt(c * c + d * d); - c /= scaleY; - d /= scaleY; - shear /= scaleY; - - // a * d - b * c should now be 1 or -1 - if (a * d < b * c) { - a = -a; - b = -b; - // We don't need c & d anymore, but if we did, we'd have to do this: - // c = -c; - // d = -d; - shear = -shear; - scaleX = -scaleX; + var a = this._a, + b = this._b, + c = this._c, + d = this._d, + det = a * d - b * c, + sqrt = Math.sqrt, + atan2 = Math.atan2, + degrees = 180 / Math.PI, + rotate, + scale, + skew; + if (a !== 0 || b !== 0) { + var r = sqrt(a * a + b * b); + rotate = Math.acos(a / r) * (b > 0 ? 1 : -1); + scale = [r, det / r]; + skew = [atan2(a * c + b * d, r * r), 0]; + } else if (c !== 0 || d !== 0) { + var s = sqrt(c * c + d * d); + // rotate = Math.PI/2 - (d > 0 ? Math.acos(-c/s) : -Math.acos(c/s)); + rotate = Math.asin(c / s) * (d > 0 ? 1 : -1); + scale = [det / s, s]; + skew = [0, atan2(a * c + b * d, s * s)]; + } else { // a = b = c = d = 0 + rotate = 0; + skew = scale = [0, 0]; } - return { - scaling: new Point(scaleX, scaleY), - rotation: -Math.atan2(b, a) * 180 / Math.PI, - shearing: shear + translation: this.getTranslation(), + rotation: rotate * degrees, + scaling: new Point(scale), + skewing: new Point(skew[0] * degrees, skew[1] * degrees) }; }, diff --git a/src/item/Item.js b/src/item/Item.js index f5693a9e..75598c7d 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -1032,7 +1032,7 @@ new function() { // // Scope to inject various item event handlers beans: true, _decompose: function() { - return this._decomposed = this._matrix.decompose(); + return this._decomposed || (this._decomposed = this._matrix.decompose()); }, /** @@ -1043,7 +1043,7 @@ new function() { // // Scope to inject various item event handlers * @type Number */ getRotation: function() { - var decomposed = this._decomposed || this._decompose(); + var decomposed = this._decompose(); return decomposed && decomposed.rotation; }, @@ -1067,7 +1067,7 @@ new function() { // // Scope to inject various item event handlers * @type Point */ getScaling: function(_dontLink) { - var decomposed = this._decomposed || this._decompose(), + var decomposed = this._decompose(), scaling = decomposed && decomposed.scaling, ctor = _dontLink ? Point : LinkedPoint; return scaling && new ctor(scaling.x, scaling.y, this, 'setScaling'); diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 388f3889..6eb90a0f 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -39,17 +39,22 @@ new function() { // See if we can decompose the matrix and can formulate it as a // simple translate/scale/rotate command sequence. var decomposed = matrix.decompose(); - if (decomposed && !decomposed.shearing) { + if (decomposed) { var parts = [], angle = decomposed.rotation, - scale = decomposed.scaling; + scale = decomposed.scaling, + skew = decomposed.skewing; if (trans && !trans.isZero()) parts.push('translate(' + formatter.point(trans) + ')'); + if (angle) + parts.push('rotate(' + formatter.number(angle) + ')'); if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1)) parts.push('scale(' + formatter.point(scale) +')'); - if (angle) - parts.push('rotate(' + formatter.number(angle) + ')'); + if (skew && skew.x) + parts.push('skewX(' + formatter.number(skew.x) + ')'); + if (skew && skew.y) + parts.push('skewY(' + formatter.number(skew.y) + ')'); attrs.transform = parts.join(' '); } else { attrs.transform = 'matrix(' + matrix.getValues().join(',') + ')'; diff --git a/test/tests/Matrix.js b/test/tests/Matrix.js index 9b5871be..6046eaf8 100644 --- a/test/tests/Matrix.js +++ b/test/tests/Matrix.js @@ -48,19 +48,19 @@ test('Decomposition: scale()', function() { } testScale(1, 1); - testScale(1, -1, -1, 1, -180); // Decomposing results in correct flipping - testScale(-1, 1); - testScale(-1, -1, 1, 1, 180); // Decomposing results in correct flipping + testScale(1, -1); + testScale(-1, 1, 1, -1, -180); // Decomposing results in correct flipping + testScale(-1, -1, 1, 1, -180); // Decomposing results in correct flipping testScale(2, 4); - testScale(2, -4, -2, 4, -180); // Decomposing results in correct flipping + testScale(2, -4); testScale(4, 2); - testScale(-4, 2); - testScale(-4, -4, 4, 4, 180); // Decomposing results in correct flipping + testScale(-4, 2, 4, -2, -180); // Decomposing results in correct flipping + testScale(-4, -4, 4, 4, -180); // Decomposing results in correct flipping }); -test('Decomposition: rotate() & scale()', function() { +test('Decomposition: scale() & rotate()', function() { function testAngleAndScale(sx, sy, a, ex, ey, ea) { - var m = new Matrix().scale(sx, sy).rotate(a), + var m = new Matrix().rotate(a).scale(sx, sy), s = 'new Matrix().scale(' + sx + ', ' + sy + ').rotate(' + a + ')'; equals(m.getRotation(), ea || a, s + '.getRotation()'); @@ -69,7 +69,7 @@ test('Decomposition: rotate() & scale()', function() { } testAngleAndScale(2, 4, 45); - testAngleAndScale(2, -4, 45, -2, 4, -135); - testAngleAndScale(-2, 4, 45); + testAngleAndScale(2, -4, 45); + testAngleAndScale(-2, 4, 45, 2, -4, -135); testAngleAndScale(-2, -4, 45, 2, 4, -135); });