Skip to content

Commit

Permalink
Proper arcs transforming with SVD
Browse files Browse the repository at this point in the history
  • Loading branch information
GreLI committed May 24, 2015
1 parent ff5e89d commit c1bf5ba
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 120 deletions.
102 changes: 13 additions & 89 deletions plugins/_path.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var regPathInstructions = /([MmLlHhVvCcSsQqTtAaZz])\s*/,
regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/,
transform2js = require('./_transforms').transform2js,
transformsMultiply = require('./_transforms').transformsMultiply,
transformArc = require('./_transforms').transformArc,
collections = require('./_collections.js'),
referencesProps = collections.referencesProps,
defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'],
Expand Down Expand Up @@ -179,8 +180,7 @@ exports.applyTransforms = function(elem, path, params) {
}))
return path;

var matrix = transformsMultiply(transform2js(elem.attr('transform').value), 9),
splittedMatrix = matrix.splitted || splitMatrix(matrix.data),
var matrix = transformsMultiply(transform2js(elem.attr('transform').value)),
stroke = elem.computedAttr('stroke'),
transformPrecision = params.transformPrecision,
newPoint, scale;
Expand Down Expand Up @@ -210,29 +210,6 @@ exports.applyTransforms = function(elem, path, params) {
}
}

// If an 'a' command can't be transformed directly, convert path to curves.
if (!splittedMatrix.isSimple && path.some(function(i) { return i.instruction == 'a' })) {
path.forEach(function(item, index, path){
var prev = index && path[index - 1];
if (item.instruction == 'a') {
var curves = a2c.apply(0, [0, 0].concat(item.data)),
items = [],
curveData;
while ((curveData = curves.splice(0,6)).length) {
items.push(prev = {
instruction: 'c',
data: curveData,
coords: [prev.coords[0] + curveData[4], prev.coords[1] + curveData[5]],
base: prev.coords
});
}
path.splice.apply(path, [index, 1].concat(items));
} else {
if (prev) item.base = prev.coords;
}
});
}

path.forEach(function(pathItem) {

if (pathItem.data) {
Expand Down Expand Up @@ -271,9 +248,17 @@ exports.applyTransforms = function(elem, path, params) {

if (pathItem.instruction == 'a') {

pathItem.data[0] *= splittedMatrix.scalex;
pathItem.data[1] *= splittedMatrix.scaley;
pathItem.data[2] += splittedMatrix.rotate;
transformArc(pathItem.data, matrix.data);

// reduce number of digits in rotation angle
if (Math.abs(pathItem.data[2]) > 80) {
var a = pathItem.data[0],
rotation = pathItem.data[2];
pathItem.data[0] = pathItem.data[1];
pathItem.data[1] = a;
pathItem.data[2] = rotation + (rotation > 0 ? -90 : 90);
}

newPoint = transformPoint(matrix.data, pathItem.data[5], pathItem.data[6]);
pathItem.data[5] = newPoint[0];
pathItem.data[6] = newPoint[1];
Expand Down Expand Up @@ -873,67 +858,6 @@ function cross(o, a, b) {
* Thanks to Dmitry Baranovskiy for his great work!
*/

function norm(a) {
return a[0] * a[0] + a[1] * a[1];
}

function normalize(a) {
var mag = Math.sqrt(norm(a));
if (a[0]) a[0] /= mag;
if (a[1]) a[1] /= mag;
}

function deg(rad) {
return rad * 180 / Math.PI % 360;
}

function determinant(matrix) {
return matrix[0] * matrix[3] - matrix[1] * matrix[2];
}

/* Splits matrix into primitive transformations
= (object) in format:
o dx (number) translation by x
o dy (number) translation by y
o scalex (number) scale by x
o scaley (number) scale by y
o shear (number) shear
o rotate (number) rotation in deg
o isSimple (boolean) could it be represented via simple transformations
*/

function splitMatrix(matrix) {
var out = {};
// translation
out.dx = matrix[4];
out.dy = matrix[5];
// scale and shear
var row = [[matrix[0] , matrix[2] ], [matrix[1] , matrix[3]]];
out.scalex = Math.sqrt(norm(row[0]));
normalize(row[0]);
out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1];
row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear];
out.scaley = Math.sqrt(norm(row[1]));
normalize(row[1]);
out.shear /= out.scaley;
if (determinant(matrix) < 0) {
out.scalex = -out.scalex;
}
// rotation
var sin = -row[0][1],
cos = row[1][1];
if (cos < 0) {
out.rotate = deg(Math.acos(cos));
if (sin < 0) {
out.rotate = 360 - out.rotate;
}
} else {
out.rotate = deg(Math.asin(sin));
}
out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate);
return out;
}

function a2c(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
// for more information of where this Math came from visit:
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
Expand Down
84 changes: 55 additions & 29 deletions plugins/_transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,31 +45,12 @@ exports.transform2js = function(transformString) {
* @param {Array} input transforms array
* @return {Array} output matrix array
*/
exports.transformsMultiply = function(transforms, precision) {

var simple = true,
scalex = 1,
scaley = 1,
rotate = 0;
exports.transformsMultiply = function(transforms) {

// convert transforms objects to the matrices
transforms = transforms.map(function(transform) {
if (transform.name === 'matrix') {
simple = false;
return transform.data;
} else if (simple) {
if (transform.name == 'scale') {
scalex *= transform.data[0];
scaley *= transform.data[1];
} else if (transform.name == 'rotate') {
if (scalex.toFixed(precision) == scaley.toFixed(precision)) {
rotate += transform.data[0];
} else {
simple = false;
}
} else if (transform.name != 'translate') {
simple = false;
}
}
return transformToMatrix(transform);
});
Expand All @@ -82,15 +63,6 @@ exports.transformsMultiply = function(transforms, precision) {
})
}

if (simple) {
transforms.splitted = {
scalex: scalex,
scaley: scaley,
rotate: rotate,
isSimple: true
}
}

return transforms;

};
Expand Down Expand Up @@ -256,6 +228,60 @@ function transformToMatrix(transform) {

};

/**
* Applies transformation to an arc. To do so, we represent ellipse as a matrix, multiply it
* by the transformation matrix and use a singular value decomposition to represent in a form
* rotate(θ)·scale(a b)·rotate(φ). This gives us new ellipse params a, b and θ.
* SVD is being done with the formulae provided by Wolffram|Alpha (svd {{m0, m2}, {m1, m3}})
*
* @param {Array} arc [a, b, rotation in deg]
* @param {Array} transform transformation matrix
* @return {Array} arc transformed input arc
*/
exports.transformArc = function(arc, transform) {

var a = arc[0],
b = arc[1],
rot = arc[2] * Math.PI / 180,
cos = Math.cos(rot),
sin = Math.sin(rot),
h = Math.pow(arc[5] * cos - arc[6] * sin, 2) / (4 * a * a) +
Math.pow(arc[5] * sin + arc[6] * cos, 2) / (4 * b * b);
if (h > 1) {
h = Math.sqrt(h);
a *= h;
b *= h;
}
var ellipse = [a * cos, a * sin, -b * sin, b * cos, 0, 0],
m = multiplyTransformMatrices(transform, ellipse),
// Decompose the new ellipse matrix
lastCol = m[2] * m[2] + m[3] * m[3],
squareSum = m[0] * m[0] + m[1] * m[1] + lastCol,
root = Math.sqrt(
(Math.pow(m[0] - m[3], 2) + Math.pow(m[1] + m[2], 2)) *
(Math.pow(m[0] + m[3], 2) + Math.pow(m[1] - m[2], 2))
);

if (!root) { // circle
arc[0] = arc[1] = Math.sqrt(squareSum / 2);
arc[2] = 0;
} else {
var majorAxisSqr = (squareSum + root) / 2,
minorAxisSqr = (squareSum - root) / 2,
major = Math.abs(majorAxisSqr - lastCol) > 1e-6,
sub = (major ? majorAxisSqr : minorAxisSqr) - lastCol,
rowsSum = m[0] * m[2] + m[1] * m[3],
term1 = m[0] * sub + m[2] * rowsSum,
term2 = m[1] * sub + m[3] * rowsSum;
arc[0] = Math.sqrt(majorAxisSqr);
arc[1] = Math.sqrt(minorAxisSqr);
arc[2] = ((major ? term2 < 0 : term1 > 0) ? -1 : 1) *
Math.acos((major ? term1 : term2) / Math.sqrt(term1 * term1 + term2 * term2)) * 180 / Math.PI;
}
return arc;

};

/**
* Multiply transformation matrices.
*
Expand Down
10 changes: 8 additions & 2 deletions test/plugins/convertPathData.11.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit c1bf5ba

Please sign in to comment.