svg-path
Version:
Manipulate SVG <path> data.
311 lines (290 loc) • 8.84 kB
JavaScript
var parsePath = require('./parsePath')
var parseCommand = require('./parseCommand')
var serializeCommand = require('./serializeCommand')
module.exports = Path
function Path (content) {
this.content = content
}
Path.prototype.abs = function () {
this.filterMap(function (command, x, y) {
if (command.relative) {
if (command.x1 != null) command.x1 += x
if (command.y1 != null) command.y1 += y
if (command.x2 != null) command.x2 += x
if (command.y2 != null) command.y2 += y
if (command.x != null) command.x += x
if (command.y != null) command.y += y
command.relative = false
}
})
}
Path.prototype.matrix = function (a, b, c, d, e, f, debug) {
for (var i=0; i < 6; i++) {
if (typeof arguments[i] !== 'number')
throw new TypeError('Matrix transformations require 6 Number arguments.')
}
this.convertArcs()
this.filterMap(function (command, x, y) {
applyMatrix(command, a, b, c, d, e, f)
})
}
Path.prototype.convertArcs = function () {
var args, curves
this.filterMap(function (command, x, y) {
if (command.type === 'A') {
return convertArc(command, x, y)
}
})
}
Path.prototype.translate = function (tx, ty) {
this.matrix(1, 0, 0, 1, tx, ty)
}
Path.prototype.scale = function (sx, sy) {
this.matrix(sx, 0, 0, sy, 0, 0)
}
Path.prototype.rotate = function (a) {
a = a * (Math.PI / 180)
this.matrix(Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0)
}
Path.prototype.skewX = function (a) {
a = a * (Math.PI / 180)
this.matrix(1, 0, Math.tan(a), 1, 0, 0)
}
Path.prototype.skewY = function (a) {
a = a * (Math.PI / 180)
this.matrix(1, Math.tan(a), 0, 1, 0, 0)
}
Path.prototype.copy = function () {
var newContent = this.content.map(function (command) {
var copy = {}
for (var attr in command) {
if (command.hasOwnProperty(attr)) copy[attr] = command[attr];
}
return copy
})
return new Path(newContent)
}
Path.prototype.split = function () {
var subpaths = [[]]
var start
function currentPath() { return subpaths[subpaths.length - 1] }
function push(command) { currentPath().push(command) }
function newPath() {
subpaths.push([])
}
this.content.forEach(function (command) {
if (command.type === 'Z') {
push(clone(command))
newPath()
} else if (command.type === 'M' && currentPath().length > 0) {
newPath()
push(clone(command))
start = command
} else {
if (command.type === 'M') {
if (command.relative) {
start = { type: 'M', relative: false,
x: command.x + start.x,
y: command.y + start.y }
} else {
start = clone(command)
}
push(start)
} else if (currentPath().length === 0) {
push({ type: 'M', relative: false, x: start.x, y: start.y })
push(clone(command))
} else {
push(clone(command))
}
}
})
if (currentPath().length === 0) subpaths.pop()
return subpaths.map(function (subpath) {
return new Path(subpath)
})
}
Path.prototype.toString = function () {
return this.content.reduce(function (result, command) {
return result + serializeCommand(command)
}, '')
}
// Iterate over the list of commands, calling the callback for each one with
// the command and the current absolute x and y.
//
// If the callback returns false, the current command will be filtered out.
// If it returns an array of commands, the current command will be replaced
// with those commands. If it returns null or undefined, nothing happens.
Path.prototype.filterMap = function (callback) {
var commands = this.content
var x, y, startX, startY, seenZ=false
var current, replacement, i, numReplaced
for (i = 0; /* do not cache length! */ i < commands.length; i++) {
current = commands[i]
if (seenZ) {
seenZ = false
replacement = callback(current, startX, startY)
} else {
replacement = callback(current, x, y)
}
if (replacement === false) {
replacement = []
}
if (replacement) {
numReplaced = replace(i, replacement)
i += numReplaced - 1
current = commands[i]
}
if (current.type === 'Z') {
seenZ = true
} else if (current.type === 'M') {
x = startX = current.x
y = startY = current.y
} else {
if (current.x != null) x = current.x
if (current.y != null) y = current.y
}
}
function replace (i, replacement) {
commands
.splice.bind(commands, i, 1)
.apply(commands, replacement)
return replacement.length
}
}
function clone (command) {
var clone = {}
for (var attr in command) {
if (command.hasOwnProperty(attr)) clone[attr] = command[attr];
}
return clone
}
function applyMatrix (cmd, a, b, c, d, e, f) {
// x' = a*x + c*y + e
// y' = b*x + d*y + f
var x = (cmd.x == null ? 0 : cmd.x)
var y = (cmd.y == null ? 0 : cmd.y)
if (cmd.x != null) {
cmd.x = x*a + y*c + e
if (nearZero(cmd.x)) cmd.x = 0
}
if (cmd.y != null) {
cmd.y = x*b + y*d + f
if (nearZero(cmd.y)) cmd.y = 0
}
if (cmd.x1 != null) {
cmd.x1 = cmd.x1*a + cmd.y1*c + e
cmd.y1 = cmd.x1*b + cmd.y1*d + f
if (nearZero(cmd.x1)) cmd.x1 = 0
if (nearZero(cmd.y1)) cmd.y1 = 0
}
if (cmd.x2 != null) {
cmd.x2 = cmd.x2*a + cmd.y2*c + e
cmd.y2 = cmd.x2*b + cmd.y2*d + f
if (nearZero(cmd.x2)) cmd.x2 = 0
if (nearZero(cmd.y2)) cmd.y2 = 0
}
}
function nearZero (num) {
return Math.abs(num) < 1e-12
}
function convertArc (command, x, y) {
if (command.relative) {
command.x += x
command.y += y
command.relative = false
}
args = a2c(x, y, command.rx, command.ry, command.x_axis_rotation, command.large_arc_flag, command.sweep_flag, command.x, command.y)
return parseCommand('C', args)
}
function a2c (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
var PI = Math.PI
// Borrowed from https://github.com/DmitryBaranovskiy/raphael/blob/4d97d4ff5350bb949b88e6d78b877f76ea8b5e24/raphael.js#L2216-L2304
// (MIT licensed; http://raphaeljs.com/license.html).
// --------------------------------------------------------------------------
// for more information of where this math came from visit:
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
var _120 = PI * 120 / 180,
rad = PI / 180 * (+angle || 0),
res = [],
xy,
rotate = function (x, y, rad) {
var X = x * Math.cos(rad) - y * Math.sin(rad),
Y = x * Math.sin(rad) + y * Math.cos(rad);
return {x: X, y: Y};
};
if (!recursive) {
xy = rotate(x1, y1, -rad);
x1 = xy.x;
y1 = xy.y;
xy = rotate(x2, y2, -rad);
x2 = xy.x;
y2 = xy.y;
var cos = Math.cos(PI / 180 * angle),
sin = Math.sin(PI / 180 * angle),
x = (x1 - x2) / 2,
y = (y1 - y2) / 2;
var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
if (h > 1) {
h = Math.sqrt(h);
rx = h * rx;
ry = h * ry;
}
var rx2 = rx * rx,
ry2 = ry * ry,
k = (large_arc_flag == sweep_flag ? -1 : 1) *
Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
cx = k * rx * y / ry + (x1 + x2) / 2,
cy = k * -ry * x / rx + (y1 + y2) / 2,
f1 = Math.asin(((y1 - cy) / ry).toFixed(9)),
f2 = Math.asin(((y2 - cy) / ry).toFixed(9));
f1 = x1 < cx ? PI - f1 : f1;
f2 = x2 < cx ? PI - f2 : f2;
f1 < 0 && (f1 = PI * 2 + f1);
f2 < 0 && (f2 = PI * 2 + f2);
if (sweep_flag && f1 > f2) {
f1 = f1 - PI * 2;
}
if (!sweep_flag && f2 > f1) {
f2 = f2 - PI * 2;
}
} else {
f1 = recursive[0];
f2 = recursive[1];
cx = recursive[2];
cy = recursive[3];
}
var df = f2 - f1;
if (Math.abs(df) > _120) {
var f2old = f2,
x2old = x2,
y2old = y2;
f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
x2 = cx + rx * Math.cos(f2);
y2 = cy + ry * Math.sin(f2);
res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
}
df = f2 - f1;
var c1 = Math.cos(f1),
s1 = Math.sin(f1),
c2 = Math.cos(f2),
s2 = Math.sin(f2),
t = Math.tan(df / 4),
hx = 4 / 3 * rx * t,
hy = 4 / 3 * ry * t,
m1 = [x1, y1],
m2 = [x1 + hx * s1, y1 - hy * c1],
m3 = [x2 + hx * s2, y2 - hy * c2],
m4 = [x2, y2];
m2[0] = 2 * m1[0] - m2[0];
m2[1] = 2 * m1[1] - m2[1];
if (recursive) {
return [m2, m3, m4].concat(res);
} else {
res = [m2, m3, m4].concat(res).join().split(',');
var newres = [];
for (var i = 0, ii = res.length; i < ii; i++) {
newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x;
}
return newres;
}
}