Matrix: Switch to a better implementation of #decompose()

This now also correctly handles skewing in SVG export.
This commit is contained in:
Jürg Lehni 2016-02-14 10:59:57 +01:00
parent e35a55fe66
commit 3ee46ffc5c
4 changed files with 52 additions and 48 deletions

View file

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

View file

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

View file

@ -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(',') + ')';

View file

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