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() { decompose: function() {
// http://dev.w3.org/csswg/css3-2d-transforms/#matrix-decomposition // 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 // 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; var a = this._a,
if (Numerical.isZero(a * d - b * c)) b = this._b,
return null; c = this._c,
d = this._d,
var scaleX = Math.sqrt(a * a + b * b); det = a * d - b * c,
a /= scaleX; sqrt = Math.sqrt,
b /= scaleX; atan2 = Math.atan2,
degrees = 180 / Math.PI,
var shear = a * c + b * d; rotate,
c -= a * shear; scale,
d -= b * shear; skew;
if (a !== 0 || b !== 0) {
var scaleY = Math.sqrt(c * c + d * d); var r = sqrt(a * a + b * b);
c /= scaleY; rotate = Math.acos(a / r) * (b > 0 ? 1 : -1);
d /= scaleY; scale = [r, det / r];
shear /= scaleY; skew = [atan2(a * c + b * d, r * r), 0];
} else if (c !== 0 || d !== 0) {
// a * d - b * c should now be 1 or -1 var s = sqrt(c * c + d * d);
if (a * d < b * c) { // rotate = Math.PI/2 - (d > 0 ? Math.acos(-c/s) : -Math.acos(c/s));
a = -a; rotate = Math.asin(c / s) * (d > 0 ? 1 : -1);
b = -b; scale = [det / s, s];
// We don't need c & d anymore, but if we did, we'd have to do this: skew = [0, atan2(a * c + b * d, s * s)];
// c = -c; } else { // a = b = c = d = 0
// d = -d; rotate = 0;
shear = -shear; skew = scale = [0, 0];
scaleX = -scaleX;
} }
return { return {
scaling: new Point(scaleX, scaleY), translation: this.getTranslation(),
rotation: -Math.atan2(b, a) * 180 / Math.PI, rotation: rotate * degrees,
shearing: shear 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, beans: true,
_decompose: function() { _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 * @type Number
*/ */
getRotation: function() { getRotation: function() {
var decomposed = this._decomposed || this._decompose(); var decomposed = this._decompose();
return decomposed && decomposed.rotation; return decomposed && decomposed.rotation;
}, },
@ -1067,7 +1067,7 @@ new function() { // // Scope to inject various item event handlers
* @type Point * @type Point
*/ */
getScaling: function(_dontLink) { getScaling: function(_dontLink) {
var decomposed = this._decomposed || this._decompose(), var decomposed = this._decompose(),
scaling = decomposed && decomposed.scaling, scaling = decomposed && decomposed.scaling,
ctor = _dontLink ? Point : LinkedPoint; ctor = _dontLink ? Point : LinkedPoint;
return scaling && new ctor(scaling.x, scaling.y, this, 'setScaling'); 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 // See if we can decompose the matrix and can formulate it as a
// simple translate/scale/rotate command sequence. // simple translate/scale/rotate command sequence.
var decomposed = matrix.decompose(); var decomposed = matrix.decompose();
if (decomposed && !decomposed.shearing) { if (decomposed) {
var parts = [], var parts = [],
angle = decomposed.rotation, angle = decomposed.rotation,
scale = decomposed.scaling; scale = decomposed.scaling,
skew = decomposed.skewing;
if (trans && !trans.isZero()) if (trans && !trans.isZero())
parts.push('translate(' + formatter.point(trans) + ')'); parts.push('translate(' + formatter.point(trans) + ')');
if (angle)
parts.push('rotate(' + formatter.number(angle) + ')');
if (!Numerical.isZero(scale.x - 1) if (!Numerical.isZero(scale.x - 1)
|| !Numerical.isZero(scale.y - 1)) || !Numerical.isZero(scale.y - 1))
parts.push('scale(' + formatter.point(scale) +')'); parts.push('scale(' + formatter.point(scale) +')');
if (angle) if (skew && skew.x)
parts.push('rotate(' + formatter.number(angle) + ')'); parts.push('skewX(' + formatter.number(skew.x) + ')');
if (skew && skew.y)
parts.push('skewY(' + formatter.number(skew.y) + ')');
attrs.transform = parts.join(' '); attrs.transform = parts.join(' ');
} else { } else {
attrs.transform = 'matrix(' + matrix.getValues().join(',') + ')'; attrs.transform = 'matrix(' + matrix.getValues().join(',') + ')';

View file

@ -48,19 +48,19 @@ test('Decomposition: scale()', function() {
} }
testScale(1, 1); testScale(1, 1);
testScale(1, -1, -1, 1, -180); // Decomposing results in correct flipping testScale(1, -1);
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(-1, -1, 1, 1, -180); // Decomposing results in correct flipping
testScale(2, 4); 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, 2); testScale(-4, 2, 4, -2, -180); // Decomposing results in correct flipping
testScale(-4, -4, 4, 4, 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) { 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 + ')'; s = 'new Matrix().scale(' + sx + ', ' + sy + ').rotate(' + a + ')';
equals(m.getRotation(), ea || a, equals(m.getRotation(), ea || a,
s + '.getRotation()'); s + '.getRotation()');
@ -69,7 +69,7 @@ test('Decomposition: rotate() & scale()', function() {
} }
testAngleAndScale(2, 4, 45); 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); testAngleAndScale(-2, -4, 45, 2, 4, -135);
}); });