svgpath
Version:
SVG path low level operations toolkit
381 lines (295 loc) • 9.72 kB
JavaScript
// SVG Path transformations library
//
// Usage:
//
// SvgPath('...')
// .translate(-150, -100)
// .scale(0.5)
// .translate(-150, -100)
// .toFixed(1)
// .toString()
//
'use strict';
// Class constructor
//
function SvgPath(pathString) {
if (!(this instanceof SvgPath)) { return new SvgPath(pathString); }
// Array of path segments.
// Each segment is array [command, param1, param2, ...]
this.segments = this.parsePath(pathString);
}
var pathCommands = /[MmZzLlHhVvCcSsQqTtAa][^MmZzLlHhVvCcSsQqTtAa]*/gm;
var pathValues = /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/g;
// Parser code is shamelessly borrowed from Raphael
// https://github.com/DmitryBaranovskiy/raphael/
//
SvgPath.prototype.parsePath = function(pathString) {
if (!pathString) { return []; }
var data = [];
var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0 };
pathString.replace(pathCommands, function(segmentString) {
var cmd = segmentString[0]; // first character of segment is a command name
var paramsStr = segmentString.substr(1); // get command parameters - all except command name
var cmdLowerCase = cmd.toLowerCase();
var params = [];
paramsStr.replace(pathValues, function(paramStr) {
params.push(+paramStr);
});
if (cmdLowerCase === "m" && params.length > 2) {
data.push([cmd].concat(params.splice(0, 2)));
cmdLowerCase = "l";
cmd = (cmd === "m") ? "l" : "L";
}
if (cmdLowerCase === "r") {
data.push([cmd].concat(params));
} else {
while (params.length >= paramCounts[cmdLowerCase]) {
data.push([cmd].concat(params.splice(0, paramCounts[cmdLowerCase])));
if (!paramCounts[cmdLowerCase]) {
break;
}
}
}
});
// First command MUST be always M or m.
// Make it always M, to avoid unnecesary checks in translate/abs
if (data[0][0].toLowerCase() !== 'm') {
data = [];
} else {
data[0][0] = 'M';
}
return data;
};
// Convert processed SVG Path back to string
//
SvgPath.prototype.toString = function() {
return [].concat.apply([], this.segments).join(' ')
// Optimizations: remove spaces around commands & before `-`
//
// We could also remove leading zeros for `0.5`-like values,
// but their count is too small to spend time for.
.replace(/ ?([achlmqrstvxz]) ?/gi, '$1')
.replace(/ \-/g, '-')
// workaround for FontForge SVG importing bug
.replace(/zm/g, 'z m');
};
// Translate coords to (x [, y])
//
SvgPath.prototype.translate = function(x, y) {
y = y || 0;
this.segments.forEach(function(segment) {
var cmd = segment[0];
// Shift coords only for commands with absolute values
if ('ACHLMRQSTVZ'.indexOf(cmd) === -1) { return; }
var name = cmd.toLowerCase();
// V is the only command, with shifted coords parity
if (name === 'v') {
segment[1] += y;
return;
}
// ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
// touch x, y only
if (name === 'a') {
segment[6] += x;
segment[7] += y;
return;
}
// All other commands have [cmd, x1, y1, x2, y2, x3, y3, ...] format
segment.forEach(function(val, i) {
if (!i) { return; } // skip command
segment[i] = i % 2 ? val + x : val + y;
});
});
return this;
};
// Scale coords to (sx [, sy])
// sy = sx if not defined
//
SvgPath.prototype.scale = function(sx, sy) {
sy = (!sy && (sy !== 0)) ? sx : sy;
this.segments.forEach(function(segment) {
var name = segment[0].toLowerCase();
// V & v are the only command, with shifted coords parity
if (name === 'v') {
segment[1] *= sy;
return;
}
// ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
// touch rx, ry, x, y only
if (name === 'a') {
segment[1] *= sx;
segment[2] *= sy;
segment[6] *= sx;
segment[7] *= sy;
return;
}
// All other commands have [cmd, x1, y1, x2, y2, x3, y3, ...] format
segment.forEach(function(val, i) {
if (!i) { return; } // skip command
segment[i] *= i % 2 ? sx : sy;
});
});
return this;
};
// Round coords with given decimal precition.
// 0 by default (to integers)
//
SvgPath.prototype.round = function(d) {
d = d || 0;
this.segments.forEach(function(segment) {
// Special processing for ARC:
// [cmd, rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
// don't touch flags and rotation
if (segment[0].toLowerCase() === 'a') {
segment[1] = +segment[1].toFixed(d);
segment[2] = +segment[2].toFixed(d);
segment[6] = +segment[6].toFixed(d);
segment[7] = +segment[7].toFixed(d);
return;
}
segment.forEach(function(val, i) {
if (!i) { return; }
segment[i] = +segment[i].toFixed(d);
});
});
return this;
};
// Apply iterator function to all segments. If function returns result,
// current segment will be replaced to array of returned segments.
// If empty array is returned, current regment will be deleted.
//
SvgPath.prototype.iterate = function(iterator) {
var segments = this.segments
, replaceQueue = []
, lastX = 0
, lastY = 0
, countourStartX = 0
, countourStartY = 0;
var index, isRelative;
segments.forEach(function(segment, index) {
var res = iterator(segment, index, lastX, lastY);
if (Array.isArray(res)) {
replaceQueue.push({ index:index, segments:res });
}
// all relative commands except Z
isRelative = 'achlmqrstv'.indexOf(segment[0]) >= 0;
var name = segment[0].toLowerCase();
// calculate absolute X and Y
if (name === 'm') {
lastX = segment[1] + (isRelative ? lastX : 0);
lastY = segment[2] + (isRelative ? lastY : 0);
countourStartX = lastX;
countourStartY = lastY;
return;
}
if (name === 'h') {
lastX = segment[1] + (isRelative ? lastX : 0);
return;
}
if (name === 'v') {
lastY = segment[1] + (isRelative ? lastY : 0);
return;
}
if (name === 'z') {
lastX = countourStartX;
lastY = countourStartY;
return;
}
lastX = segment[segment.length - 2] + (isRelative ? lastX : 0);
lastY = segment[segment.length - 1] + (isRelative ? lastY : 0);
});
// Replace segments if iterator return results
var len = replaceQueue.length;
for (index = len - 1; index >= 0 ; index--) {
segments.splice(replaceQueue[index].index, 1);
// FIXME: Replace cycle with `apply`
var newSegLen = replaceQueue[index].segments.length;
for (var newSegIndex = newSegLen - 1; newSegIndex >= 0 ; newSegIndex--) {
segments.splice(replaceQueue[index].index, 0, replaceQueue[index].segments[newSegIndex]);
}
}
return this;
};
// Converts segments from relative to absolute
//
SvgPath.prototype.abs = function () {
this.iterate(function(segment, index, x, y) {
var name = segment[0];
// Skip absolute commands
if ('ACHLMRQSTVZ'.indexOf(name) >= 0) { return; }
// absolute commands has uppercase names
segment[0] = name.toUpperCase();
// V is the only command, with shifted coords parity
if (name === 'v') {
segment[1] += y;
return;
}
// ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
// touch x, y only
if (name === 'a') {
segment[6] += x;
segment[7] += y;
return;
}
segment.forEach(function(val, i) {
if (!i) { return; } // skip command
segment[i] = i % 2 ? val + x : val + y; // odd values are X, even - Y
});
});
return this;
};
// Converts segments from absolute to relative
//
SvgPath.prototype.rel = function () {
this.iterate(function(segment, index, x, y) {
var name = segment[0];
// Skip relative commands
if ('ACHLMRQSTVZ'.indexOf(name) === -1) { return; }
// relative commands has lowercase names
segment[0] = name.toLowerCase();
// V is the only command, with shifted coords parity
if (name === 'V') {
segment[1] -= y;
return;
}
// ARC is: ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
// touch x, y only
if (name === 'A') {
segment[6] -= x;
segment[7] -= y;
return;
}
segment.forEach(function(val, i) {
if (!i) { return; } // skip command
segment[i] = i % 2 ? val - x : val - y; // odd values are X, even - Y
});
});
return this;
};
// Converts smooth curves (with missed control point) to generic curves
//
SvgPath.prototype.unshort = function() {
var self = this;
var prevControlX, prevControlY;
var curControlX, curControlY;
this.iterate(function(segment, index, x, y) {
var name = segment[0];
if (name === 'T') { // qubic curve
segment[0] = 'Q';
prevControlX = self.segments[index - 1][0] === 'Q' ? self.segments[index - 1][1] : x;
curControlX = 2 * (x || 0) - (prevControlX || 0);
prevControlY = self.segments[index - 1][0] === 'Q' ? self.segments[index - 1][2] : y;
curControlY = 2 * (y || 0) - (prevControlY || 0);
segment.splice(1, 0, curControlX, curControlY);
} else if (name === 'S') { // quadratic curve
segment[0] = 'C';
prevControlX = self.segments[index - 1][0] === 'C' ? self.segments[index - 1][3] : x;
curControlX = 2 * (x || 0) - (prevControlX || 0);
prevControlY = self.segments[index - 1][0] === 'C' ? self.segments[index - 1][4] : y;
curControlY = 2 * (y || 0) - (prevControlY || 0);
segment.splice(1, 0, curControlX, curControlY);
}
});
return this;
};
module.exports = SvgPath;