Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7c15f21

Browse files
committedDec 22, 2015
Added mathods for finding/setting the bounding box of a path
1 parent ee7c988 commit 7c15f21

File tree

4 files changed

+503
-1
lines changed

4 files changed

+503
-1
lines changed
 

Diff for: ‎lib/box.js

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
'use strict';
2+
3+
// precision for consider cubic polynom as quadratic one
4+
var epsilon = 0.00000001;
5+
6+
// New box : empty or parsed from string like '-10 10 300 400'
7+
//
8+
function Box(s) {
9+
if (!(this instanceof Box)) { return new Box(s); }
10+
11+
// minX, minY, maxX, maxY : are not defined yet
12+
// but empty box has 0 x 0 size
13+
this.width = this.height = 0;
14+
15+
// parse the string parameter
16+
if (s && s.constructor === String) {
17+
var a = s.trim().split(/\s+/).map(parseFloat);
18+
19+
this.addX(a[0]).addX(a[0] + a[2]).addY(a[1]).addY(a[1] + a[3]);
20+
}
21+
22+
return this;
23+
}
24+
25+
// check if box is not defined yet
26+
//
27+
Box.prototype.isUndefined = function () {
28+
return (typeof this.minX === 'undefined') || (typeof this.minY === 'undefined');
29+
};
30+
31+
// add new X coordinate
32+
//
33+
Box.prototype.addX = function (x) {
34+
if (typeof this.minX === 'undefined') {
35+
this.minX = this.maxX = x;
36+
this.width = 0;
37+
} else {
38+
this.minX = Math.min(this.minX, x);
39+
this.maxX = Math.max(this.maxX, x);
40+
this.width = this.maxX - this.minX;
41+
}
42+
43+
return this;
44+
};
45+
46+
// add new Y coordinate
47+
//
48+
Box.prototype.addY = function (y) {
49+
if (typeof this.minY === 'undefined') {
50+
this.minY = this.maxY = y;
51+
this.height = 0;
52+
} else {
53+
this.minY = Math.min(this.minY, y);
54+
this.maxY = Math.max(this.maxY, y);
55+
this.height = this.maxY - this.minY;
56+
}
57+
58+
return this;
59+
};
60+
61+
// add new point
62+
//
63+
Box.prototype.addPoint = function (x, y) {
64+
return this.addX(x).addY(y);
65+
};
66+
67+
68+
// ------------------------------
69+
// return [min,max]
70+
// of A[0] * (1-t) * (1-t) + A[1] * 2 * (1-t) * t + A[2] * t * t
71+
// for t in [0,1]
72+
// ------------------------------
73+
function minmaxQ(A) {
74+
var min = Math.min(A[0], A[2]),
75+
max = Math.max(A[0], A[2]);
76+
77+
if (A[1] > A[0] ? A[2] >= A[1] : A[2] <= A[1]) {
78+
// if no extremum in ]0,1[
79+
return [ min, max ];
80+
}
81+
82+
// check if the extremum E is min or max
83+
var E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]);
84+
return E < min ? [ E, max ] : [ min, E ];
85+
}
86+
87+
// add new quadratic curve to X coordinate
88+
//
89+
Box.prototype.addXQ = function (A) {
90+
var minmax = minmaxQ(A);
91+
92+
return this.addX(minmax[0]).addX(minmax[1]);
93+
};
94+
95+
// add new quadratic curve to Y coordinate
96+
//
97+
Box.prototype.addYQ = function (A) {
98+
var minmax = minmaxQ(A);
99+
100+
return this.addY(minmax[0]).addY(minmax[1]);
101+
};
102+
103+
104+
// ------------------------------
105+
// return [min,max]
106+
// of A[0] * (1-t) * (1-t) * (1-t) + A[1] * 3 * (1-t) * (1-t) * t + A[2] * 3 * (1-t) * t * t + A[3] * t * t * t
107+
// for t in [0,1]
108+
// ------------------------------
109+
function minmaxC(A) {
110+
// if the polynomial is (almost) quadratic and not cubic
111+
var K = A[0] - 3 * A[1] + 3 * A[2] - A[3];
112+
if (Math.abs(K) < epsilon) {
113+
return minmaxQ([ A[0], -0.5 * A[0] + 1.5 * A[1], A[0] - 3 * A[1] + 3 * A[2] ]);
114+
}
115+
116+
117+
// the reduced discriminant of the derivative
118+
var T = -A[0] * A[2] + A[0] * A[3] - A[1] * A[2] - A[1] * A[3] + A[1] * A[1] + A[2] * A[2];
119+
120+
// if the polynomial is monotone in [0,1]
121+
if (T <= 0) {
122+
return [ Math.min(A[0], A[3]), Math.max(A[0], A[3]) ];
123+
}
124+
var S = Math.sqrt(T);
125+
126+
// potential extrema
127+
var max = Math.max(A[0], A[3]),
128+
min = Math.min(A[0], A[3]);
129+
130+
var L = A[0] - 2 * A[1] + A[2];
131+
// check local extrema
132+
for (var R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) {
133+
if (R > 0 && R < 1) {
134+
// if the extrema is for R in [0,1]
135+
var Q = A[0] * (1 - R) * (1 - R) * (1 - R) +
136+
A[1] * 3 * (1 - R) * (1 - R) * R +
137+
A[2] * 3 * (1 - R) * R * R +
138+
A[3] * R * R * R;
139+
if (Q < min) { min = Q; }
140+
if (Q > max) { max = Q; }
141+
}
142+
}
143+
144+
return [ min, max ];
145+
}
146+
147+
// add new cubic curve to X coordinate
148+
//
149+
Box.prototype.addXC = function (A) {
150+
var minmax = minmaxC(A);
151+
152+
return this.addX(minmax[0]).addX(minmax[1]);
153+
};
154+
155+
// add new cubic curve to Y coordinate
156+
//
157+
Box.prototype.addYC = function (A) {
158+
var minmax = minmaxC(A);
159+
160+
return this.addY(minmax[0]).addY(minmax[1]);
161+
};
162+
163+
// return a string like '-10 10 300 400'
164+
//
165+
Box.prototype.toViewBoxString = function (pr) {
166+
// if empty box
167+
if (this.isUndefined()) {
168+
return '0 0 0 0';
169+
}
170+
171+
// else
172+
return ((typeof pr === 'undefined') ?
173+
[ this.minX, this.minY, this.width, this.height ]
174+
:
175+
[
176+
this.minX.toFixed(pr), this.minY.toFixed(pr),
177+
this.width.toFixed(pr), this.height.toFixed(pr)
178+
]
179+
).join(' ');
180+
};
181+
182+
// return the transform that translate and scale to fit in a box
183+
// controlled by the following parameters :
184+
// - type:
185+
// - fit(=none) : scale the box (aspect ratio is not preserved) to fit in the box
186+
// - meet (the default) : scale the box (aspect ratio is preserved) as much as possible
187+
// to cover the destination box
188+
// - slice : scale the box (aspect ratio is preserved) as less as possible to cover the destination box
189+
// - move : translate only (no scale) the box according to x???y??? parameter
190+
// - position x(Min|Mid|Max)Y(Min|Mid|Max).
191+
// example : matrixToBox(src, '100 0 200 300 meet xMidYMin')
192+
//
193+
Box.prototype.matrixToBox = function (parameters) {
194+
var dst = new Box(parameters.match(/(-|\d|\.|\s)+/)[0]);
195+
196+
// get the action (default is 'meet')
197+
var action = ((parameters + 'meet').match(/(fit|none|meet|slice|move)/))[0];
198+
199+
if (action === 'none') { // for compatibility with 'preserveAspectRatio'
200+
action = 'fit';
201+
}
202+
203+
// set the scale factors based on the action
204+
var rx, ry;
205+
switch (action) {
206+
case 'fit':
207+
rx = this.width ? dst.width / this.width : 1;
208+
ry = this.height ? dst.height / this.height : 1;
209+
break;
210+
case 'slice' :
211+
if (this.width !== 0 && this.height !== 0) {
212+
rx = ry = Math.max(dst.width / this.width, dst.height / this.height);
213+
break;
214+
}
215+
// else falls through
216+
case 'meet' :
217+
rx = ry = (this.width === 0 && this.height === 0) ? 1 :
218+
Math.min(dst.width / this.width, dst.height / this.height);
219+
break;
220+
case 'move':
221+
rx = ry = 1;
222+
break;
223+
}
224+
225+
// get the position from string like 'xMidYMax'
226+
var position = {};
227+
position.X = ((parameters + 'xMid').match(/x(Min|Mid|Max)/i))[1].toLowerCase();
228+
position.Y = ((parameters + 'YMid').match(/Y(Min|Mid|Max)/i))[1].toLowerCase();
229+
230+
// variable that helps to loop over the two boxes
231+
var origin = {},
232+
box = {};
233+
box.src = this;
234+
box.dst = dst;
235+
236+
// set the 'origin' of the two boxes based on the position parameters
237+
for (var c = 'X', i = 1; i <= 2; c = 'Y', i++) {
238+
for (var b = 'src', j = 1; j <= 2; b = 'dst', j++) {
239+
switch (position[c]) {
240+
case 'min':
241+
origin[b + c] = box[b]['min' + c];
242+
break;
243+
case 'mid':
244+
origin[b + c] = (box[b]['min' + c] + box[b]['max' + c]) / 2;
245+
break;
246+
case 'max':
247+
origin[b + c] = box[b]['max' + c];
248+
break;
249+
}
250+
}
251+
}
252+
253+
// return the matrix that is equivalent to
254+
// .translate(-box.src.originX,-box.src.originY)
255+
// .scale(rx,ry)
256+
// .translate(box.dst.originX,box.dst.originY);
257+
return [ rx, 0, 0, ry, origin.dstX - rx * origin.srcX, origin.dstY - ry * origin.srcY ];
258+
};
259+
260+
module.exports = Box;

Diff for: ‎lib/svgpath.js

+86-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var transformParse = require('./transform_parse');
1818
var matrix = require('./matrix');
1919
var a2c = require('./a2c');
2020
var ellipse = require('./ellipse');
21+
var box = require('./box');
2122

2223

2324
// Class constructor
@@ -38,6 +39,27 @@ function SvgPath(path) {
3839
this.__stack = [];
3940
}
4041

42+
// copy path
43+
//
44+
SvgPath.prototype.copy = function () {
45+
var newP = new SvgPath('');
46+
var i;
47+
48+
// copy segments
49+
for (i = 0; i < this.segments.length; i++) {
50+
newP.segments[i] = this.segments[i].slice(0);
51+
}
52+
53+
// copy err
54+
newP.err = this.err;
55+
56+
// copy __stack
57+
for (i = 0; i < this.__stack.length; i++) {
58+
newP.matrix(this.__stack[i].toArray());
59+
}
60+
61+
return newP;
62+
};
4163

4264
SvgPath.prototype.__matrix = function (m) {
4365
var self = this,
@@ -211,7 +233,7 @@ SvgPath.prototype.matrix = function (m) {
211233
};
212234

213235

214-
// Transform path according to "transform" attr of SVG spec
236+
// Transform path according to 'transform' attr of SVG spec
215237
//
216238
SvgPath.prototype.transform = function (transformString) {
217239
if (!transformString.trim()) {
@@ -591,4 +613,67 @@ SvgPath.prototype.unshort = function () {
591613
};
592614

593615

616+
// return the bounding box of an absolute normalized path.
617+
// normalized = without arc segments (A) and without reduced segments (S,T)
618+
//
619+
SvgPath.prototype.getBoundingBox = function () {
620+
var bb = box();
621+
622+
if (this.segments.length === 0) {
623+
return bb;
624+
}
625+
626+
var P = this.copy().abs().unarc().unshort();
627+
628+
P.iterate(function (s, i, x, y) {
629+
switch (s[0]) {
630+
case 'H':
631+
bb.addX(s[1]);
632+
break;
633+
case 'V':
634+
bb.addY(s[1]);
635+
break;
636+
case 'M':
637+
case 'L':
638+
bb.addX(s[1]);
639+
bb.addY(s[2]);
640+
break;
641+
case 'Q':
642+
bb.addXQ([ x, s[1], s[3] ]);
643+
bb.addYQ([ y, s[2], s[4] ]);
644+
break;
645+
case 'C':
646+
bb.addXC([ x, s[1], s[3], s[5] ]);
647+
bb.addYC([ y, s[2], s[4], s[6] ]);
648+
break;
649+
} // end switch
650+
}, true); // end iterate
651+
652+
return bb;
653+
};
654+
655+
656+
// return a string that can be used as a viewBox for the path
657+
//
658+
SvgPath.prototype.toViewBoxString = function (pr) {
659+
return this.getBoundingBox().toViewBoxString(pr);
660+
};
661+
662+
// translate and scale the path to fit in a box
663+
// controlled by the following parameters :
664+
// - type:
665+
// - fit(=none) : scale the path (aspect ratio is not preserved) to fit the box
666+
// - meet (the default) : scale the path (aspect ratio is preserved) as much as possible
667+
// to fit the entire path in the box
668+
// - slice : scale the path (aspect ratio is preserved) as less as possible to cover the box
669+
// - move : translate only (no scale) the path according to x???y??? parameter
670+
// - position x(Min|Mid|Max)Y(Min|Mid|Max).
671+
// example : .toBox('-10 10 300 400 meet xMidYMin')
672+
//
673+
SvgPath.prototype.toBox = function (parameters) {
674+
this.matrix(this.getBoundingBox().matrixToBox(parameters));
675+
676+
return this;
677+
};
678+
594679
module.exports = SvgPath;

Diff for: ‎test/api.js

+30
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,36 @@ describe('API', function () {
563563
'M25 25A15 15 0 0 1 50 50'
564564
);
565565
});
566+
});
567+
568+
describe('bounding box', function () {
569+
it('get the bounding box', function () {
570+
assert.equal(
571+
svgpath('M10,10 c 10,0 10,10 0,10 s -10,0 0,10 q 10,10 15 20 t 10,0 a25,25 -30 0,1 50,-25z').toViewBoxString(2),
572+
'2.50 9.54 82.50 45.46'
573+
);
574+
});
566575

576+
it('matrix to fit in a box', function () {
577+
assert.equal(
578+
svgpath('M10,10 h10 v20').toBox('0 0 100 100').toViewBoxString(),
579+
'25 0 50 100'
580+
);
581+
582+
assert.equal(
583+
svgpath('M10,10 h10 v20').toBox('0 0 100 100 slice xMinYMax').toViewBoxString(),
584+
'0 -100 100 200'
585+
);
586+
587+
assert.equal(
588+
svgpath('M10,10 h10 v20').toBox('0 0 100 100 fit').toViewBoxString(),
589+
'0 0 100 100'
590+
);
591+
592+
assert.equal(
593+
svgpath('M10,10 h10 v20').toBox('0 0 100 100 move xMaxYMid').toViewBoxString(),
594+
'90 40 10 20'
595+
);
596+
});
567597
});
568598
});

Diff for: ‎test/box.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use strict';
2+
3+
4+
var assert = require('assert');
5+
var box = require('../lib/box');
6+
7+
var b, m;
8+
9+
describe('Box', function () {
10+
11+
it('default box is undefined with size 0 x 0', function () {
12+
b = box();
13+
14+
assert(b.isUndefined());
15+
assert.equal(b.width, 0);
16+
assert.equal(b.height, 0);
17+
});
18+
19+
it('parse from string', function () {
20+
b = box('-1 2 4 5');
21+
22+
assert.equal(b.minX, -1);
23+
assert.equal(b.maxX, 3);
24+
assert.equal(b.width, 4);
25+
assert.equal(b.minY, 2);
26+
assert.equal(b.maxY, 7);
27+
assert.equal(b.height, 5);
28+
});
29+
30+
it('add a point', function () {
31+
b = box().addPoint(1, 1);
32+
33+
assert.equal(b.minX, 1);
34+
assert.equal(b.maxX, 1);
35+
assert.equal(b.width, 0);
36+
assert.equal(b.minY, 1);
37+
assert.equal(b.maxY, 1);
38+
assert.equal(b.height, 0);
39+
40+
b.addX(2);
41+
42+
assert.equal(b.minX, 1);
43+
assert.equal(b.maxX, 2);
44+
assert.equal(b.width, 1);
45+
assert.equal(b.minY, 1);
46+
assert.equal(b.maxY, 1);
47+
assert.equal(b.height, 0);
48+
49+
b.addY(3);
50+
51+
assert.equal(b.minX, 1);
52+
assert.equal(b.maxX, 2);
53+
assert.equal(b.width, 1);
54+
assert.equal(b.minY, 1);
55+
assert.equal(b.maxY, 3);
56+
assert.equal(b.height, 2);
57+
58+
b.addPoint(4, -5);
59+
60+
assert.equal(b.minX, 1);
61+
assert.equal(b.maxX, 4);
62+
assert.equal(b.width, 3);
63+
assert.equal(b.minY, -5);
64+
assert.equal(b.maxY, 3);
65+
assert.equal(b.height, 8);
66+
});
67+
68+
it('add quadratic curve', function () {
69+
b = box().addXQ([ 0, 3, 1 ]);
70+
71+
assert.equal(b.minX, 0);
72+
assert.equal(b.maxX, 1.8);
73+
assert.equal(b.width, 1.8);
74+
75+
b = box().addYQ([ 0, -2, 1 ]);
76+
77+
assert.equal(b.minY, -0.8);
78+
assert.equal(b.maxY, 1);
79+
assert.equal(b.height, 1.8);
80+
});
81+
82+
it('add cubic curve', function () {
83+
b = box().addXC([ 0, -70, 210, 100 ]);
84+
85+
assert.equal(Math.round(b.minX), -11);
86+
assert.equal(Math.round(b.maxX), 126);
87+
assert.equal(Math.round(b.width), 137);
88+
89+
b = box().addYC([ 0, 1, 2, 3 ]);
90+
91+
assert.equal(b.minY, 0);
92+
assert.equal(b.maxY, 3);
93+
assert.equal(b.height, 3);
94+
});
95+
96+
it('view box', function () {
97+
b = box().addXC([ 0, -70, 210, 100 ]).addYC([ 0, -30, 70, 40 ]);
98+
99+
assert.equal(b.toViewBoxString(0), '-11 -6 137 51');
100+
101+
b = box('-10 20 30 50');
102+
103+
assert.equal(b.minX, -10);
104+
assert.equal(b.maxX, 20);
105+
assert.equal(b.width, 30);
106+
assert.equal(b.minY, 20);
107+
assert.equal(b.maxY, 70);
108+
assert.equal(b.height, 50);
109+
});
110+
111+
it('matrix to put in a box', function () {
112+
b = box('-10 0 40 50');
113+
114+
m = b.matrixToBox('0 0 100 200'); // default is meet xMidYMid
115+
assert.deepEqual(m, [ 2.5, 0, 0, 2.5, 25, 37.5 ]);
116+
117+
m = b.matrixToBox('0 0 100 200 slice xMinYMax');
118+
assert.deepEqual(m, [ 4, 0, 0, 4, 40, 0 ]);
119+
120+
m = b.matrixToBox('0 0 100 200 fit');
121+
assert.deepEqual(m, [ 2.5, 0, 0, 4, 25, 0 ]);
122+
123+
m = b.matrixToBox('0 0 100 200 move xMinYmid');
124+
assert.deepEqual(m, [ 1, 0, 0, 1, 10, 75 ]);
125+
});
126+
127+
});

0 commit comments

Comments
 (0)
Please sign in to comment.