From e438ac822373d910d1549d22ed531abf977c0593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 8 Feb 2013 23:02:20 -0800 Subject: [PATCH] Implement proper matrix decomposition and use it in SvgExport. --- src/basic/Matrix.js | 58 ++++++++++++++++------ src/svg/SvgExport.js | 28 +++++------ test/tests/Matrix.js | 114 ++++++++++++++++++++++--------------------- 3 files changed, 114 insertions(+), 86 deletions(-) diff --git a/src/basic/Matrix.js b/src/basic/Matrix.js index 3d712632..6c7f0695 100644 --- a/src/basic/Matrix.js +++ b/src/basic/Matrix.js @@ -468,33 +468,61 @@ var Matrix = this.Matrix = Base.extend(/** @lends Matrix# */{ ); }, + decompose: function() { + // http://dev.w3.org/csswg/css3-2d-transforms/#matrix-decomposition + // http://stackoverflow.com/questions/4361242/ + var a = this._a, b = this._b, c = this._c, d = this._d; + if (Numerical.isZero(a * d - b * c)) + return {}; + + 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 use c & d anymore, but this would be correct: + // c = -c; + // d = -d; + shear = -shear; + scaleX = -scaleX; + } + + return { + scaling: Point.create(scaleX, scaleY), + rotation: -Math.atan2(b, a) * 180 / Math.PI, + shearing: shear, + translation: Point.create(this._tx, this._ty) + }; + }, + getTranslation: function() { - return Point.create(this._tx, this._ty); + return this.decompose().translation; }, getScaling: function() { - // http://math.stackexchange.com/questions/13150/ - // http://stackoverflow.com/questions/4361242/ - var hor = Math.sqrt(this._a * this._a + this._b * this._b), - ver = Math.sqrt(this._c * this._c + this._d * this._d); - return Point.create( - this._a < 0 ? -hor : hor, - this._d < 0 ? -ver : ver); + return this.decompose().scaling; }, /** - * The rotation angle of the matrix. If a non-uniform rotation is applied as - * a result of a shear() or scale() command, undefined is returned, as the - * resulting transformation cannot be expressed in one rotation angle. + * The rotation angle of the matrix. * * @type Number * @bean */ getRotation: function() { - var angle1 = -Math.atan2(this._b, this._a), - angle2 = Math.atan2(this._c, this._d); - return Math.abs(angle1 - angle2) < /*#=*/ Numerical.EPSILON - ? angle1 * 180 / Math.PI : undefined; + return this.decompose().rotation; }, /** diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 67213ba8..92009813 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -44,7 +44,8 @@ new function() { function getTransform(item, coordinates) { var matrix = item._matrix, - trans = matrix.getTranslation(), + decomposed = matrix.decompose(), + trans = decomposed.translation, attrs = {}; if (coordinates) { // If the item suppports x- and y- coordinates, we're taking out the @@ -61,27 +62,22 @@ new function() { } if (matrix.isIdentity()) return attrs; - // See if we can formulate this matrix as simple scale / rotate commands - // Note: getScaling() returns values also when it's not a simple scale, - // but angle is only != null if it is, so check for that. - // TODO: We disable transformation detection for now, until - // Matrix#getRotation() and Matrix#getScaling() work correctly for all - // angles and values of scaling. - var angle = null, // matrix.getRotation(), - parts = []; - if (angle != null) { - matrix = matrix.clone().scale(1, -1); + // See if we can formulate the decomposed matrix as a simple + // translate/scale/rotate command sequence. + if (!decomposed.shearing) { + var parts = [], + angle = decomposed.rotation, + scale = decomposed.scaling; if (trans && !trans.isZero()) parts.push('translate(' + formatPoint(trans) + ')'); - if (angle) - parts.push('rotate(' + formatFloat(angle) + ')'); - var scale = matrix.getScaling(); if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1)) parts.push('scale(' + formatPoint(scale) +')'); + if (angle) + parts.push('rotate(' + formatFloat(angle) + ')'); + attrs.transform = parts.join(' '); } else { - parts.push('matrix(' + matrix.getValues().join(',') + ')'); + attrs.transform = 'matrix(' + matrix.getValues().join(',') + ')'; } - attrs.transform = parts.join(' '); return attrs; } diff --git a/test/tests/Matrix.js b/test/tests/Matrix.js index 92c5e60a..b6c43461 100644 --- a/test/tests/Matrix.js +++ b/test/tests/Matrix.js @@ -11,67 +11,56 @@ */ module('Matrix'); -test('getRotation()', function() { - equals(function() { - return new Matrix().rotate(45).getRotation(); - }, 45); +test('Decomposition: rotate()', function() { + function testAngle(angle, expected) { + equals(new Matrix().rotate(angle).getRotation(), + Base.pick(expected, angle), + 'new Matrix().rotate(' + angle + ').getRotation()', + Numerical.TOLERANCE); + equals(new Matrix().rotate(angle).getScaling(), + new Point(1, 1), + 'new Matrix().rotate(' + angle + ').getScaling()'); + } - equals(function() { - return new Matrix().rotate(90).getRotation(); - }, 90); - - equals(function() { - return new Matrix().rotate(180).getRotation(); - }, 180); - - equals(function() { - return new Matrix().rotate(270).getRotation(); - }, -90, null, Numerical.TOLERANCE); - - equals(function() { - return new Matrix().rotate(-45).getRotation(); - }, -45); - - equals(function() { - return new Matrix().rotate(-90).getRotation(); - }, -90); - - equals(function() { - return new Matrix().rotate(-180).getRotation(); - }, -180); - - equals(function() { - return new Matrix().rotate(-270).getRotation(); - }, 90, null, Numerical.TOLERANCE); + testAngle(0); + testAngle(1); + testAngle(45); + testAngle(90); + testAngle(135); + testAngle(180); + testAngle(270, -90); + testAngle(-1); + testAngle(-45); + testAngle(-90); + testAngle(-135); + testAngle(-180); + testAngle(-270, 90); }); -test('getScaling()', function() { - equals(function() { - return new Matrix().scale(1, 1).getScaling(); - }, new Point(1, 1)); +test('Decomposition: scale()', function() { + function testScale(sx, sy) { + var flipped = sx < 0 && sy < 0; + equals(new Matrix().scale(sx, sy).getScaling(), + new Point(flipped ? -sx : sx, flipped ? -sy : sy), + 'new Matrix().scale(' + sx + ', ' + sy + ').getScaling()'); + equals(new Matrix().scale(sx, sy).getRotation(), + flipped ? 180 : 0, + 'new Matrix().scale(' + sx + ', ' + sy + ').getRotation()', + Numerical.TOLERANCE); + } - equals(function() { - return new Matrix().scale(1, -1).getScaling(); - }, new Point(1, -1)); - - equals(function() { - return new Matrix().scale(-1, 1).getScaling(); - }, new Point(-1, 1)); - - equals(function() { - return new Matrix().scale(2, -4).getScaling(); - }, new Point(2, -4)); - - equals(function() { - return new Matrix().scale(-4, 2).getScaling(); - }, new Point(-4, 2)); - - equals(function() { - return new Matrix().scale(-4, -4).getScaling(); - }, new Point(-4, -4)); + testScale(1, 1); + testScale(1, -1); + testScale(-1, 1); + testScale(-1, -1); + testScale(2, 4); + testScale(2, -4); + testScale(4, 2); + testScale(-4, 2); + testScale(-4, -4); }); -test('getRotation() & getScaling()', function() { +test('Decomposition: rotate() & scale()', function() { equals(function() { return new Matrix().scale(2, 4).rotate(45).getScaling(); }, new Point(2, 4)); @@ -88,4 +77,19 @@ test('getRotation() & getScaling()', function() { return new Matrix().scale(2, -4).rotate(45).getRotation(); }, 45); + equals(function() { + return new Matrix().scale(-2, 4).rotate(45).getScaling(); + }, new Point(-2, 4)); + + equals(function() { + return new Matrix().scale(-2, 4).rotate(45).getRotation(); + }, 45); + + equals(function() { + return new Matrix().scale(-2, -4).rotate(45).getScaling(); + }, new Point(-2, -4)); + + equals(function() { + return new Matrix().scale(-2, -4).rotate(45).getRotation(); + }, 45); });