Implement proper matrix decomposition and use it in SvgExport.

This commit is contained in:
Jürg Lehni 2013-02-08 23:02:20 -08:00
parent aec87f4ed1
commit e438ac8223
3 changed files with 114 additions and 86 deletions

View file

@ -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;
},
/**

View file

@ -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;
}

View file

@ -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);
});