svg-boundings
Version:
Get bounding rect of SVG shape elements.
838 lines (769 loc) • 22.8 kB
JavaScript
'use strict';
var CurveBounding = require('./curve_bounding');
var ElementObject = require('./element_object');
var Path = require('./path');
var Helper = require('../util/helper');
function boundingRectOfLine(line) {
line = ElementObject(line);
var x1 = line.x1;
var y1 = line.y1;
var x2 = line.x2;
var y2 = line.y2;
return {
left: Math.min(x1, x2),
top: Math.min(y1, y2),
right: Math.max(x1, x2),
bottom: Math.max(y1, y2),
width: Math.abs(x1 - x2),
height: Math.abs(y1 - y2)
};
}
function boundingRectOfRect(rect) {
rect = ElementObject(rect);
var w = rect.width;
var h = rect.height;
var l = rect.x || 0;
var t = rect.y || 0;
var r = l + w;
var b = t + h;
var transform = rect.transform;
var matrix;
if (transform) {
matrix = Helper.transformToMatrix(transform);
return Helper.boundingUnderTransform(matrix, t, r, b, l);
}
return {
left: l,
top: t,
right: r,
bottom: b,
width: w,
height: h
};
}
function boundingRectOfCircle(circle) {
circle = ElementObject(circle);
var cx = circle.cx || 0;
var cy = circle.cy || 0;
var r = circle.r;
return {
left: cx - r,
top: cy - r,
right: cx + r,
bottom: cy + r,
width: 2 * r,
height: 2 * r
};
}
function boundingRectOfEllipse(ellipse, shouldReturnTrueBounding) {
ellipse = ElementObject(ellipse);
var cx = ellipse.cx || 0;
var cy = ellipse.cy || 0;
var rx = ellipse.rx;
var ry = ellipse.ry;
var l = cx - rx;
var t = cy - ry;
var r = l + 2 * rx;
var b = t + 2 * ry;
var transform = ellipse.transform;
var matrix;
if (transform) {
matrix = Helper.transformToMatrix(transform);
if (shouldReturnTrueBounding) {
// https://img.alicdn.com/tfscom/TB1iZqOPFXXXXceXpXXXXXXXXXX.jpg
var ma = matrix.e(1, 1);
var mb = matrix.e(2, 1);
var mc = matrix.e(1, 2);
var md = matrix.e(2, 2);
var me = matrix.e(1, 3);
var mf = matrix.e(2, 3);
var denominator = ma*md-mb*mc;
var A = ry*ry*md*md+rx*rx*mb*mb;
var B = -2*(mc*md*ry*ry+ma*mb*rx*rx);
var C = ry*ry*mc*mc+rx*rx*ma*ma;
var D = 2*ry*ry*(mc*md*mf-md*md*me)+2*rx*rx*(ma*mb*mf-mb*mb*me) - 2*(cx*ry*ry*md-cy*rx*rx*mb)*denominator;
var E = 2*ry*ry*(mc*md*me-mc*mc*mf)+2*rx*rx*(ma*mb*me-ma*ma*mf) + 2*(cx*ry*ry*mc-cy*rx*rx*ma)*denominator;
var F = ry*ry*(mc*mc*mf*mf-2*mc*md*me*mf+md*md*me*me)+rx*rx*(ma*ma*mf*mf-2*ma*mb*me*mf+mb*mb*me*me) + (2*cx*ry*ry*(md*me-mc*mf)+2*cy*rx*rx*(ma*mf-mb*me))*denominator + (ry*ry*cx*cx+rx*rx*cy*cy-rx*rx*ry*ry)*Math.pow(denominator, 2);
var a = 4*A*C-B*B;
var b1 = 4*A*E-2*B*D;
var c1 = 4*A*F-D*D;
var d1 = b1*b1-4*a*c1;
var b2 = 4*C*D-2*B*E;
var c2 = 4*C*F-E*E;
var d2 = b2*b2-4*a*c2;
var tb1 = (0-b1+Math.sqrt(d1))/(2*a);
var tb2 = (0-b1-Math.sqrt(d1))/(2*a);
var lr1 = (0-b2+Math.sqrt(d2))/(2*a);
var lr2 = (0-b2-Math.sqrt(d2))/(2*a);
return {
left: Math.min(lr1, lr2),
top: Math.min(tb1, tb2),
right: Math.max(lr1, lr2),
bottom: Math.max(tb1, tb2),
_wh: function() {
delete this._wh;
this.width = this.right - this.left;
this.height = this.bottom - this.top;
return this;
}
}._wh();
}
else return Helper.boundingUnderTransform(matrix, t, r, b, l);
}
return {
left: l,
top: t,
right: r,
bottom: b,
width: 2 * rx,
height: 2 * ry
};
}
function boundingRectOfPolygon(polygon) {
polygon = ElementObject(polygon);
var points = polygon.points.trim().replace(/\r\n|\n|\r/gm, ',').replace(/\s+/g, ',').split(',').map(parseFloat);
var l = Number.POSITIVE_INFINITY;
var r = Number.NEGATIVE_INFINITY;
var t = Number.POSITIVE_INFINITY;
var b = Number.NEGATIVE_INFINITY;
for (var i = 0; i < points.length; i+=2) {
if (l > points[i]) l = points[i];
if (r < points[i]) r = points[i];
if (t > points[i+1]) t = points[i+1];
if (b < points[i+1]) b = points[i+1];
}
return {
left: l,
top: t,
right: r,
bottom: b,
width: r - l,
height: b - t
};
}
function boundingRectOfPolyline(polyline) {
polyline = ElementObject(polyline);
return boundingRectOfPolygon(polyline);
}
// This method returns the bounding box of the path.
// Unless shouldReturnTrueBounding is set to a truthy value,
// it only checks each point, not the actual drawn path,
// meaning the bounding box may be larger than the actual
// bounding box. The reason is:
// 1. we don't need the exact bounding box;
// 2. all the browsers calculate this way;
// 3. it is easier to calculate.
// This method assumes the d property of the path is valid.
// Since SVG is exported from Illustrator, I assume this condition
// is always met.
// Things ignored:
// 1. ~~the xAxisRotation property of A/a command;~~
// 2. M/m command checking.
// Because Illustrator doesn't export A/a command as well as useless
// M/m commands, we are good here.
// If the path's d property contains A/a commands, it will be converted
// to a bezier curve before calculating the bounding box
function boundingRectOfPath(path, shouldReturnTrueBounding) {
path = ElementObject(path);
var d = path.d.replace(/\r\n|\n|\r/gm, '');
// expand transform for true bounding calculation
if (
(path.transform && shouldReturnTrueBounding) ||
/[Aa]/.test(d)
) {
d = Path.expandPathTransform(d, path.transform);
}
var x = 0, y = 0;
var commands = [];
var params, potentialCp; // cp for control point
var l = Number.POSITIVE_INFINITY;
var r = Number.NEGATIVE_INFINITY;
var t = Number.POSITIVE_INFINITY;
var b = Number.NEGATIVE_INFINITY;
var getArgs = Path.argsFromPathD;
var checkX = function(val) {
if (val < l) l = val;
if (val > r) r = val;
};
var checkY = function(val) {
if (val < t) t = val;
if (val > b) b = val;
};
// Get all commands first
var i = 0, c = '';
while (c = d.charAt(i++)) {
if (/[mlhvaqtcsz]/i.test(c)) commands.push(c);
}
// The shift() is used to throw away strings come before the first command
params = d.replace(/[mlhvaqtcsz]/ig, '#').split('#');
params.shift();
params.forEach(function(str, idx) {
var command = commands[idx];
if (/z/i.test(command)) return;
// Get arguments of each command
var args = getArgs(str);
// Different commands have different arguments
// Here's a quick review
// M m - x y
// L l - x y
// H h - x
// V v - y
// A a - rx ry xAxisRotation largeArc sweep x y
// Q q - x1 y1 x y
// T t - x y
// C c - x1 y1 x2 y2 x y
// S s - x2 y2 x y
// S/s needs access to the points of previous C/c command
// T/t needs access to the points of previous Q/q command
// Here "previous" means right before the target command
var i, trueBounds, cpx1, cpy1, cpx2, cpy2;
if (/[ML]/.test(command)) {
for (i = 0; i < args.length; i += 2) {
x = args[i];
y = args[i+1];
checkX(x);
checkY(y);
}
}
else if (/[ml]/.test(command)) {
for (i = 0; i < args.length; i += 2) {
x += args[i];
y += args[i+1];
checkX(x);
checkY(y);
}
}
else if (command === 'C') {
for (i = 0; i < args.length; i += 6) {
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
args[i], args[i+1],
args[i+2], args[i+3],
args[i+4], args[i+5]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(args[i]);
checkY(args[i+1]);
checkX(args[i+2]);
checkY(args[i+3]);
checkX(args[i+4]);
checkY(args[i+5]);
}
potentialCp = [
args[i+4] * 2 - args[i+2],
args[i+5] * 2 - args[i+3]
];
x = args[i+4];
y = args[i+5];
}
}
else if (command === 'c') {
for (i = 0; i < args.length; i += 6) {
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
x+args[i], y+args[i+1],
x+args[i+2], y+args[i+3],
x+args[i+4], y+args[i+5]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(x+args[i+0]);
checkY(y+args[i+1]);
checkX(x+args[i+2]);
checkY(y+args[i+3]);
checkX(x+args[i+4]);
checkY(y+args[i+5]);
}
potentialCp = [
2*(x+args[i+4]) - (x+args[i+2]),
2*(y+args[i+5]) - (y+args[i+3])
];
x += args[i+4];
y += args[i+5];
}
}
else if (command === 'S') {
if (shouldReturnTrueBounding) {
if (/[cs]/i.test(commands[idx - 1])) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
potentialCp[0], potentialCp[1],
args[0], args[1],
args[2], args[3]
);
}
else {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
x, y,
args[0], args[1],
args[2], args[3]
);
}
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
if (/[cs]/i.test(commands[idx - 1])) {
checkX(potentialCp[0]);
checkY(potentialCp[1]);
}
checkX(args[0]);
checkY(args[1]);
checkX(args[2]);
checkY(args[3]);
}
potentialCp = [
2*args[2] - args[0],
2*args[3] - args[1]
];
x = args[2];
y = args[3];
for (i = 4; i < args.length; i += 4) {
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
potentialCp[0], potentialCp[1],
args[i], args[i+1],
args[i+2], args[i+3]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(potentialCp[0]);
checkY(potentialCp[1]);
checkX(args[i]);
checkY(args[i+1]);
checkX(args[i+2]);
checkY(args[i+3]);
}
potentialCp = [
2*args[i+2] - args[i],
2*args[i+3] - args[i+1]
];
x = args[i+2];
y = args[i+3];
}
}
else if (command === 's') {
if (shouldReturnTrueBounding) {
if (/[cs]/i.test(commands[idx - 1])) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
potentialCp[0], potentialCp[1],
x+args[0], y+args[1],
x+args[2], y+args[3]
);
}
else {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
x, y,
x+args[0], y+args[1],
x+args[2], y+args[3]
);
}
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
if (/[cs]/i.test(commands[idx - 1])) {
checkX(potentialCp[0]);
checkY(potentialCp[1]);
}
checkX(x+args[0]);
checkY(y+args[1]);
checkX(x+args[2]);
checkY(y+args[3]);
}
potentialCp = [
2*(x+args[2]) - (x+args[0]),
2*(y+args[3]) - (y+args[1])
];
x += args[2];
y += args[3];
for (i = 4; i < args.length; i += 4) {
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
potentialCp[0], potentialCp[1],
x+args[i], y+args[i+1],
x+args[i+2], y+args[i+3]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(potentialCp[0]);
checkY(potentialCp[1]);
checkX(x+args[i]);
checkY(y+args[i+1]);
checkX(x+args[i+2]);
checkY(y+args[i+3]);
}
potentialCp = [
2*(x+args[i+2]) - (x+args[i]),
2*(y+args[i+3]) - (y+args[i+1])
];
x += args[i+2];
y += args[i+3];
}
}
else if (command === 'H') {
for (i = 0; i < args.length; i++) {
x = args[i];
checkX(x);
}
}
else if (command === 'h') {
for (i = 0; i < args.length; i++) {
x += args[i];
checkX(x);
}
}
else if (command === 'V') {
for (i = 0; i < args.length; i++) {
y = args[i];
checkY(y);
}
}
else if (command === 'v') {
for (i = 0; i < args.length; i++) {
y += args[i];
checkY(y);
}
}
else if (command === 'Q') {
for (i = 0; i < args.length; i += 4) {
// convert the one quadratic curve control point to
// two bezier curve control points using the formula
// cubicControlX1 = quadraticStartX + 2/3 * (quadraticControlX - quadraticStartX)
// cubicControlY1 = quadraticStartY + 2/3 * (quadraticControlY - quadraticStartY)
// cubicControlX2 = quadraticEndX + 2/3 * (quadraticControlX - quadraticEndX)
// cubicControlY2 = quadraticEndY + 2/3 * (quadraticControlY - quadraticEndY)
cpx1 = x + 2/3 * (args[i] - x);
cpy1 = y + 2/3 * (args[i+1] - y);
cpx2 = args[i+2] + 2/3 * (args[i] - args[i+2]);
cpy2 = args[i+3] + 2/3 * (args[i+1] - args[i+3]);
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
cpx1, cpy1,
cpx2, cpy2,
args[i+2], args[i+3]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(cpx1);
checkY(cpy1);
checkX(cpx2);
checkY(cpy2);
checkX(args[i+2]);
checkY(args[i+3]);
}
potentialCp = [
2*args[i+2] - args[i],
2*args[i+3] - args[i+1]
];
x = args[i+2];
y = args[i+3];
}
}
else if (command === 'q') {
for (i = 0; i < args.length; i += 4) {
cpx1 = x + 2/3 * args[i];
cpy1 = y + 2/3 * args[i+1];
cpx2 = x+args[i+2] + 2/3 * (args[i] - args[i+2]);
cpy2 = y+args[i+3] + 2/3 * (args[i+1] - args[i+3]);
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
cpx1, cpy1,
cpx2, cpy2,
x+args[i+2], y+args[i+3]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(cpx1);
checkY(cpy1);
checkX(cpx2);
checkY(cpy2);
checkX(x+args[i+2]);
checkY(y+args[i+3]);
}
potentialCp = [
2*(x+args[i+2]) - (x+args[i]),
2*(y+args[i+3]) - (y+args[i+1])
];
x += args[i+2];
y += args[i+3];
}
}
else if (command === 'T') {
if (/[qt]/i.test(commands[idx - 1])) {
cpx1 = x + 2/3 * (potentialCp[0] - x);
cpy1 = y + 2/3 * (potentialCp[1] - y);
cpx2 = args[0] + 2/3 * (potentialCp[0] - args[0]);
cpy2 = args[1] + 2/3 * (potentialCp[1] - args[1]);
potentialCp = [
2*args[0] - potentialCp[0],
2*args[1] - potentialCp[1]
];
}
else {
cpx1 = x;
cpy1 = y;
cpx2 = args[0] + 2/3 * (x - args[0]);
cpy2 = args[1] + 2/3 * (y - args[1]);
potentialCp = [
2*args[0] - x,
2*args[1] - y
];
}
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
cpx1, cpy1,
cpx2, cpy2,
args[0], args[1]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(cpx1);
checkY(cpy1);
checkX(cpx2);
checkY(cpy2);
checkX(args[0]);
checkY(args[1]);
}
x = args[0];
y = args[1];
for (i = 2; i < args.length; i += 2) {
cpx1 = x + 2/3 * (potentialCp[0] - x);
cpy1 = y + 2/3 * (potentialCp[1] - y);
cpx2 = args[i] + 2/3 * (potentialCp[0] - args[i]);
cpy2 = args[i+1] + 2/3 * (potentialCp[1] - args[i+1]);
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
cpx1, cpy1,
cpx2, cpy2,
args[i], args[i+1]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(cpx1);
checkY(cpy1);
checkX(cpx2);
checkY(cpy2);
checkX(args[i]);
checkY(args[i+1]);
}
potentialCp = [
2*args[i] - potentialCp[0],
2*args[i+1] - potentialCp[1]
];
x = args[i];
y = args[i+1];
}
}
else if (command === 't') {
if (/[qt]/i.test(commands[idx - 1])) {
cpx1 = x + 2/3 * (potentialCp[0] - x);
cpy1 = y + 2/3 * (potentialCp[1] - y);
cpx2 = x+args[0] + 2/3 * (potentialCp[0] -x-args[0]);
cpy2 = y+args[1] + 2/3 * (potentialCp[1] -y-args[1]);
potentialCp = [
2*(x+args[0]) - potentialCp[0],
2*(y+args[1]) - potentialCp[1]
];
}
else {
cpx1 = x;
cpy1 = y;
cpx2 = x+args[0] - 2/3 * args[0];
cpy2 = y+args[1] - 2/3 * args[1];
potentialCp = [
2*(x+args[0]) - x,
2*(y+args[1]) - y
];
}
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
cpx1, cpy1,
cpx2, cpy2,
x+args[0], y+args[1]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(cpx1);
checkY(cpy1);
checkX(cpx2);
checkY(cpy2);
checkX(x+args[0]);
checkY(y+args[1]);
}
x += args[0];
y += args[1];
for (i = 2; i < args.length; i += 2) {
cpx1 = x + 2/3 * (potentialCp[0] - x);
cpy1 = y + 2/3 * (potentialCp[1] - y);
cpx2 = x+args[i] + 2/3 * (potentialCp[0] -x-args[i]);
cpy2 = y+args[i+1] + 2/3 * (potentialCp[1] -y-args[i+1]);
if (shouldReturnTrueBounding) {
trueBounds = CurveBounding.calculate(
CurveBounding.Mode.STANDARD,
x, y,
cpx1, cpy1,
cpx2, cpy2,
x+args[i], y+args[i+1]
);
checkX(trueBounds[0]); // MIN_X
checkX(trueBounds[4]); // MAX_X
checkY(trueBounds[1]); // MIN_Y
checkY(trueBounds[5]); // MAX_Y
}
else {
checkX(cpx1);
checkY(cpy1);
checkX(cpx2);
checkY(cpy2);
checkX(x+args[i]);
checkY(y+args[i+1]);
}
potentialCp = [
2*(x+args[i]) - potentialCp[0],
2*(y+args[i+1]) - potentialCp[1]
];
x += args[i];
y += args[i+1];
}
}
else if (command === 'A') {
for (var i = 0; i < args.length; i += 7) {
x = args[i+5];
y = args[i+6];
checkX(x);
checkY(y);
}
}
else if (command === 'a') {
for (var i = 0; i < args.length; i += 7) {
x += args[i+5];
y += args[i+6];
checkX(x);
checkY(y);
}
}
});
// if not calculation true bounding, just apply the transform to the bounding
// which is calculated only using the d attribute
// currently this only works the same as the browser if all the control points
// are within the true bounding box
var matrix;
if (path.transform && !shouldReturnTrueBounding) {
matrix = Helper.transformToMatrix(path.transform);
return Helper.boundingUnderTransform(matrix, t, r, b, l);
}
return {
left: l,
top: t,
right: r,
bottom: b,
width: r - l,
height: b - t
};
}
function boundingRectOfShape(shape, needTrueBounding) {
var elementObj = ElementObject(shape);
if (!elementObj) return null;
var bounding;
switch(elementObj.type) {
case 'path':
bounding = boundingRectOfPath(elementObj, needTrueBounding);
break;
case 'polygon':
bounding = boundingRectOfPolygon(elementObj);
break;
case 'rect':
bounding = boundingRectOfRect(elementObj);
break;
case 'ellipse':
bounding = boundingRectOfEllipse(elementObj, needTrueBounding);
break;
case 'circle':
bounding = boundingRectOfCircle(elementObj);
break;
case 'polyline':
bounding = boundingRectOfPolyline(elementObj);
break;
case 'line':
bounding = boundingRectOfLine(elementObj);
break;
}
return bounding;
}
module.exports = {
line: boundingRectOfLine,
rect: boundingRectOfRect,
circle: boundingRectOfCircle,
ellipse: boundingRectOfEllipse,
polygon: boundingRectOfPolygon,
polyline: boundingRectOfPolyline,
path: boundingRectOfPath,
shape: boundingRectOfShape
};