Skip to content

Commit

Permalink
Merge pull request #15 from kpym/arctransformfix
Browse files Browse the repository at this point in the history
New version of bug #13 fix
  • Loading branch information
Vitaly Puzrin committed Dec 21, 2015
2 parents d288ed6 + 14efddb commit eb4dc56
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 60 deletions.
83 changes: 83 additions & 0 deletions lib/ellipse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

// The precision used to consider an ellipse as a circle
//
var epsilon = 0.0001;

// To convert degree in radians
//
var torad = Math.PI / 180;

// Class constructor :
// an ellipse centred at 0 with radii rx,ry and x - axis - angle ax.
//
function Ellipse(rx, ry, ax) {
if (!(this instanceof Ellipse)) { return new Ellipse(rx, ry, ax); }
this.rx = rx;
this.ry = ry;
this.ax = ax;
}

// Apply a linear transform m to the ellipse
// m is an array representing a matrix :
// - -
// | m[0] m[2] |
// | m[1] m[3] |
// - -
//
Ellipse.prototype.transform = function (m) {
// We consider the current ellipse as image of the unit circle
// by first scale(rx,ry) and then rotate(ax) ...
// So we apply ma = m x rotate(ax) x scale(rx,ry) to the unit circle.
var c = Math.cos(this.ax * torad), s = Math.sin(this.ax * torad);
var ma = [ this.rx * (m[0] * c + m[2] * s),
this.rx * (m[1] * c + m[3] * s),
this.ry * (-m[0] * s + m[2] * c),
this.ry * (-m[1] * s + m[3] * c) ];
// ma * transpose(ma) = [ J L ]
// [ L K ]
// L is calculated later (if the image is not a circle)
var J = ma[0] * ma[0] + ma[2] * ma[2], K = ma[1] * ma[1] + ma[3] * ma[3];

// the discriminant of the characteristic polynomial of ma * transpose(ma)
var D = ((ma[0] - ma[3]) * (ma[0] - ma[3]) + (ma[2] + ma[1]) * (ma[2] + ma[1])) *
((ma[0] + ma[3]) * (ma[0] + ma[3]) + (ma[2] - ma[1]) * (ma[2] - ma[1]));
// the "mean eigenvalue"
var JK = (J + K) / 2;
// check if the image is (almost) a circle
if (D < epsilon * JK) {
// if it is
this.rx = this.ry = Math.sqrt(JK);
this.ax = 0;
} else {
// if it is not a circle
var L = ma[0] * ma[1] + ma[2] * ma[3];
D = Math.sqrt(D);
// {l1,l2} = the two eigen values of ma * transpose(ma)
var l1 = JK + D / 2,
l2 = JK - D / 2;
// the x - axis - rotation angle is the argument of the l1 - eigenvector
this.ax = (L === 0 && l1 === K) ? 90 : Math.atan(
Math.abs(L) > Math.abs(l1 - K) ? (l1 - J) / L : L / (l1 - K)
) * 180 / Math.PI;
// if ax > 0 => rx = sqrt(l1), ry = sqrt(l2), else exchange axes and ax += 90
if (this.ax >= 0) { // if ax in [0,90]
this.rx = Math.sqrt(l1);
this.ry = Math.sqrt(l2);
} else { // if ax in ]-90,0[ => exchange axes
this.ax += 90;
this.rx = Math.sqrt(l2);
this.ry = Math.sqrt(l1);
}
}

return this;
};

// Check if the ellipse is (almost) degenerate, i.e. rx = 0 or ry = 0
//
Ellipse.prototype.isDegenerate = function () {
return (this.rx < epsilon * this.ry || this.ry < epsilon * this.rx);
};

module.exports = Ellipse;
54 changes: 14 additions & 40 deletions lib/svgpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var pathParse = require('./path_parse');
var transformParse = require('./transform_parse');
var matrix = require('./matrix');
var a2c = require('./a2c');
var ellipse = require('./ellipse');


// Class constructor
Expand Down Expand Up @@ -81,49 +82,22 @@ SvgPath.prototype.__matrix = function (m) {
return [];
}

// Decompose affine matrix and apply components to arc params
// http://math.stackexchange.com/questions/78137/decomposition-of-a-nonsquare-affine-matrix

// Fill cache if empty
if (!ma) {
ma = m.toArray();

sx = Math.sqrt(Math.pow(ma[0], 2) + Math.pow(ma[2], 2));
arc2line = true;

if (sx !== 0) {
sy = (ma[0] * ma[3] - ma[1] * ma[2]) / sx;
if (sy !== 0) {
if (ma[0] === 0) {
angle = ma[1] < 0 ? -90 : 90;
} else {
angle = Math.atan(ma[1] / ma[0]) * 180 / Math.PI;
}
arc2line = false;
}
}
}

// If scaleX / scaleY / rx / ry === 0 - replace arc with line
if (arc2line || s[1] === 0 || s[2] === 0) {
p = m.calc(s[6], s[7], s[0] === 'a');
result = [ (s[0] === 'a') ? 'l' : 'L', p[0], p[1] ];
break;
}

result = s.slice();

// Apply scale to rx & ry only
result[1] = s[1] * sx;
result[2] = s[2] * sy;

// Apply rotation angle to x-axis-rotation & normalize result
result[3] = (s[3] + angle) % 360;
// Transform rx, ry and the x-axis-rotation
var e = ellipse(s[1], s[2], s[3]).transform(m.toArray());

// Transform end point as usual (without translation for relative notation)
p = m.calc(s[6], s[7], s[0] === 'a');
result[6] = p[0];
result[7] = p[1];

// if the resulting ellipse is (almost) a segment ...
if (e.isDegenerate()) {
// replace the arc by a line
result = [ s[0] === 'a' ? 'l' : 'L', p[0], p[1] ];
} else {
// if it is a real ellipse
// s[0], s[4] and s[5] are not modified
result = [ s[0], e.rx, e.ry, e.ax, s[4], s[5], p[0], p[1] ];
}

break;

case 'm':
Expand Down
52 changes: 32 additions & 20 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,13 @@ describe('API', function () {

it('should handle arcs', function () {
assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').scale(2, 1.5).toString(),
'M80 45a40 60-45 0 1 40 75'
svgpath('M40 30a20 40 -45 0 1 20 50').scale(2, 1.5).round().toString(),
'M80 45a72 34 32.04 0 1 40 75'
);

assert.equal(
svgpath('M40 30A20 40 -45 0 1 20 50').scale(2, 1.5).toString(),
'M80 45A40 60-45 0 1 40 75'
svgpath('M40 30A20 40 -45 0 1 20 50').scale(2, 1.5).round().toString(),
'M80 45A72 34 32.04 0 1 40 75'
);
});
});
Expand All @@ -242,28 +242,28 @@ describe('API', function () {
describe('rotate', function () {
it('rotate by 90 degrees about point(10, 10)', function () {
assert.equal(
svgpath('M10 10L15 10').rotate(90, 10, 10).round(0).toString(),
svgpath('M10 10L15 10').rotate(90, 10, 10).round().toString(),
'M10 10L10 15'
);
});

it('rotate by -90 degrees about point (0,0)', function () {
assert.equal(
svgpath('M0 10L0 20').rotate(-90).round(0).toString(),
svgpath('M0 10L0 20').rotate(-90).round().toString(),
'M10 0L20 0'
);
});

it('rotate abs arc', function () {
assert.equal(
svgpath('M 100 100 A 90 30 0 1 1 200 200').rotate(45).round(0).toString(),
svgpath('M 100 100 A 90 30 0 1 1 200 200').rotate(45).round().toString(),
'M0 141A90 30 45 1 1 0 283'
);
});

it('rotate rel arc', function () {
assert.equal(
svgpath('M 100 100 a 90 30 15 1 1 200 200').rotate(20).round(0).toString(),
svgpath('M 100 100 a 90 30 15 1 1 200 200').rotate(20).round().toString(),
'M60 128a90 30 35 1 1 119 257'
);
});
Expand Down Expand Up @@ -293,6 +293,18 @@ describe('API', function () {
'M5 5C20 30 10 15 30 15'
);
});

it('should handle arcs', function () {
assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').matrix([ 1.5, 0.5, 0.5, 1.5, 10, 15 ]).round().toString(),
'M85 80a80 20 45 0 1 55 85'
);

assert.equal(
svgpath('M40 30A20 40 -45 0 1 20 50').matrix([ 1.5, 0.5, 0.5, 1.5, 10, 15 ]).round().toString(),
'M85 80A80 20 45 0 1 65 100'
);
});
});


Expand All @@ -306,14 +318,14 @@ describe('API', function () {

it('scale + rotate', function () {
assert.equal(
svgpath('M0 0 L 10 10 20 10').scale(2, 3).rotate(90).round(0).toString(),
svgpath('M0 0 L 10 10 20 10').scale(2, 3).rotate(90).round().toString(),
'M0 0L-30 20-30 40'
);
});

it('empty', function () {
assert.equal(
svgpath('M0 0 L 10 10 20 10').translate(0).scale(1).rotate(0, 10, 10).round(0).toString(),
svgpath('M0 0 L 10 10 20 10').translate(0).scale(1).rotate(0, 10, 10).round().toString(),
'M0 0L10 10 20 10'
);
});
Expand Down Expand Up @@ -358,13 +370,13 @@ describe('API', function () {

it('should handle arcs', function () {
assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').translate(10, 15).toString(),
'M50 45a20 40-45 0 1 20 50'
svgpath('M40 30a20 40 -45 0 1 20 50').translate(10, 15).round().toString(),
'M50 45a40 20 45 0 1 20 50'
);

assert.equal(
svgpath('M40 30A20 40 -45 0 1 20 50').translate(10, 15).toString(),
'M50 45A20 40-45 0 1 30 65'
svgpath('M40 30A20 40 -45 0 1 20 50').translate(10, 15).round().toString(),
'M50 45A40 20 45 0 1 30 65'
);
});
});
Expand Down Expand Up @@ -525,23 +537,23 @@ describe('API', function () {

it('rotate to +/- 90 degree', function () {
assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').rotate(90).round(2).toString(),
svgpath('M40 30a20 40 -45 0 1 20 50').rotate(90).round().toString(),
'M-30 40a20 40 45 0 1-50 20'
);

assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').matrix([ 0, 1, -1, 0, 0, 0 ]).toString(),
svgpath('M40 30a20 40 -45 0 1 20 50').matrix([ 0, 1, -1, 0, 0, 0 ]).round().toString(),
'M-30 40a20 40 45 0 1-50 20'
);

assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').rotate(-90).round(2).toString(),
'M30-40a20 40-135 0 1 50-20'
svgpath('M40 30a20 40 -45 0 1 20 50').rotate(-90).round().toString(),
'M30-40a20 40 45 0 1 50-20'
);

assert.equal(
svgpath('M40 30a20 40 -45 0 1 20 50').matrix([ 0, -1, 1, 0, 0, 0 ]).toString(),
'M30-40a20 40-135 0 1 50-20'
svgpath('M40 30a20 40 -45 0 1 20 50').matrix([ 0, -1, 1, 0, 0, 0 ]).round().toString(),
'M30-40a20 40 45 0 1 50-20'
);
});
});
Expand Down

0 comments on commit eb4dc56

Please sign in to comment.