Skip to content

Commit

Permalink
Merge 7c15f21 into ee7c988
Browse files Browse the repository at this point in the history
  • Loading branch information
kpym committed Dec 22, 2015
2 parents ee7c988 + 7c15f21 commit 619c7fc
Show file tree
Hide file tree
Showing 4 changed files with 503 additions and 1 deletion.
260 changes: 260 additions & 0 deletions lib/box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
'use strict';

// precision for consider cubic polynom as quadratic one
var epsilon = 0.00000001;

// New box : empty or parsed from string like '-10 10 300 400'
//
function Box(s) {
if (!(this instanceof Box)) { return new Box(s); }

// minX, minY, maxX, maxY : are not defined yet
// but empty box has 0 x 0 size
this.width = this.height = 0;

// parse the string parameter
if (s && s.constructor === String) {
var a = s.trim().split(/\s+/).map(parseFloat);

this.addX(a[0]).addX(a[0] + a[2]).addY(a[1]).addY(a[1] + a[3]);
}

return this;
}

// check if box is not defined yet
//
Box.prototype.isUndefined = function () {
return (typeof this.minX === 'undefined') || (typeof this.minY === 'undefined');
};

// add new X coordinate
//
Box.prototype.addX = function (x) {
if (typeof this.minX === 'undefined') {
this.minX = this.maxX = x;
this.width = 0;
} else {
this.minX = Math.min(this.minX, x);
this.maxX = Math.max(this.maxX, x);
this.width = this.maxX - this.minX;
}

return this;
};

// add new Y coordinate
//
Box.prototype.addY = function (y) {
if (typeof this.minY === 'undefined') {
this.minY = this.maxY = y;
this.height = 0;
} else {
this.minY = Math.min(this.minY, y);
this.maxY = Math.max(this.maxY, y);
this.height = this.maxY - this.minY;
}

return this;
};

// add new point
//
Box.prototype.addPoint = function (x, y) {
return this.addX(x).addY(y);
};


// ------------------------------
// return [min,max]
// of A[0] * (1-t) * (1-t) + A[1] * 2 * (1-t) * t + A[2] * t * t
// for t in [0,1]
// ------------------------------
function minmaxQ(A) {
var min = Math.min(A[0], A[2]),
max = Math.max(A[0], A[2]);

if (A[1] > A[0] ? A[2] >= A[1] : A[2] <= A[1]) {
// if no extremum in ]0,1[
return [ min, max ];
}

// check if the extremum E is min or max
var E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]);
return E < min ? [ E, max ] : [ min, E ];
}

// add new quadratic curve to X coordinate
//
Box.prototype.addXQ = function (A) {
var minmax = minmaxQ(A);

return this.addX(minmax[0]).addX(minmax[1]);
};

// add new quadratic curve to Y coordinate
//
Box.prototype.addYQ = function (A) {
var minmax = minmaxQ(A);

return this.addY(minmax[0]).addY(minmax[1]);
};


// ------------------------------
// return [min,max]
// 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
// for t in [0,1]
// ------------------------------
function minmaxC(A) {
// if the polynomial is (almost) quadratic and not cubic
var K = A[0] - 3 * A[1] + 3 * A[2] - A[3];
if (Math.abs(K) < epsilon) {
return minmaxQ([ A[0], -0.5 * A[0] + 1.5 * A[1], A[0] - 3 * A[1] + 3 * A[2] ]);
}


// the reduced discriminant of the derivative
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];

// if the polynomial is monotone in [0,1]
if (T <= 0) {
return [ Math.min(A[0], A[3]), Math.max(A[0], A[3]) ];
}
var S = Math.sqrt(T);

// potential extrema
var max = Math.max(A[0], A[3]),
min = Math.min(A[0], A[3]);

var L = A[0] - 2 * A[1] + A[2];
// check local extrema
for (var R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) {
if (R > 0 && R < 1) {
// if the extrema is for R in [0,1]
var Q = A[0] * (1 - R) * (1 - R) * (1 - R) +
A[1] * 3 * (1 - R) * (1 - R) * R +
A[2] * 3 * (1 - R) * R * R +
A[3] * R * R * R;
if (Q < min) { min = Q; }
if (Q > max) { max = Q; }
}
}

return [ min, max ];
}

// add new cubic curve to X coordinate
//
Box.prototype.addXC = function (A) {
var minmax = minmaxC(A);

return this.addX(minmax[0]).addX(minmax[1]);
};

// add new cubic curve to Y coordinate
//
Box.prototype.addYC = function (A) {
var minmax = minmaxC(A);

return this.addY(minmax[0]).addY(minmax[1]);
};

// return a string like '-10 10 300 400'
//
Box.prototype.toViewBoxString = function (pr) {
// if empty box
if (this.isUndefined()) {
return '0 0 0 0';
}

// else
return ((typeof pr === 'undefined') ?
[ this.minX, this.minY, this.width, this.height ]
:
[
this.minX.toFixed(pr), this.minY.toFixed(pr),
this.width.toFixed(pr), this.height.toFixed(pr)
]
).join(' ');
};

// return the transform that translate and scale to fit in a box
// controlled by the following parameters :
// - type:
// - fit(=none) : scale the box (aspect ratio is not preserved) to fit in the box
// - meet (the default) : scale the box (aspect ratio is preserved) as much as possible
// to cover the destination box
// - slice : scale the box (aspect ratio is preserved) as less as possible to cover the destination box
// - move : translate only (no scale) the box according to x???y??? parameter
// - position x(Min|Mid|Max)Y(Min|Mid|Max).
// example : matrixToBox(src, '100 0 200 300 meet xMidYMin')
//
Box.prototype.matrixToBox = function (parameters) {
var dst = new Box(parameters.match(/(-|\d|\.|\s)+/)[0]);

// get the action (default is 'meet')
var action = ((parameters + 'meet').match(/(fit|none|meet|slice|move)/))[0];

if (action === 'none') { // for compatibility with 'preserveAspectRatio'
action = 'fit';
}

// set the scale factors based on the action
var rx, ry;
switch (action) {
case 'fit':
rx = this.width ? dst.width / this.width : 1;
ry = this.height ? dst.height / this.height : 1;
break;
case 'slice' :
if (this.width !== 0 && this.height !== 0) {
rx = ry = Math.max(dst.width / this.width, dst.height / this.height);
break;
}
// else falls through
case 'meet' :
rx = ry = (this.width === 0 && this.height === 0) ? 1 :
Math.min(dst.width / this.width, dst.height / this.height);
break;
case 'move':
rx = ry = 1;
break;
}

// get the position from string like 'xMidYMax'
var position = {};
position.X = ((parameters + 'xMid').match(/x(Min|Mid|Max)/i))[1].toLowerCase();
position.Y = ((parameters + 'YMid').match(/Y(Min|Mid|Max)/i))[1].toLowerCase();

// variable that helps to loop over the two boxes
var origin = {},
box = {};
box.src = this;
box.dst = dst;

// set the 'origin' of the two boxes based on the position parameters
for (var c = 'X', i = 1; i <= 2; c = 'Y', i++) {
for (var b = 'src', j = 1; j <= 2; b = 'dst', j++) {
switch (position[c]) {
case 'min':
origin[b + c] = box[b]['min' + c];
break;
case 'mid':
origin[b + c] = (box[b]['min' + c] + box[b]['max' + c]) / 2;
break;
case 'max':
origin[b + c] = box[b]['max' + c];
break;
}
}
}

// return the matrix that is equivalent to
// .translate(-box.src.originX,-box.src.originY)
// .scale(rx,ry)
// .translate(box.dst.originX,box.dst.originY);
return [ rx, 0, 0, ry, origin.dstX - rx * origin.srcX, origin.dstY - ry * origin.srcY ];
};

module.exports = Box;
87 changes: 86 additions & 1 deletion lib/svgpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var transformParse = require('./transform_parse');
var matrix = require('./matrix');
var a2c = require('./a2c');
var ellipse = require('./ellipse');
var box = require('./box');


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

// copy path
//
SvgPath.prototype.copy = function () {
var newP = new SvgPath('');
var i;

// copy segments
for (i = 0; i < this.segments.length; i++) {
newP.segments[i] = this.segments[i].slice(0);
}

// copy err
newP.err = this.err;

// copy __stack
for (i = 0; i < this.__stack.length; i++) {
newP.matrix(this.__stack[i].toArray());
}

return newP;
};

SvgPath.prototype.__matrix = function (m) {
var self = this,
Expand Down Expand Up @@ -211,7 +233,7 @@ SvgPath.prototype.matrix = function (m) {
};


// Transform path according to "transform" attr of SVG spec
// Transform path according to 'transform' attr of SVG spec
//
SvgPath.prototype.transform = function (transformString) {
if (!transformString.trim()) {
Expand Down Expand Up @@ -591,4 +613,67 @@ SvgPath.prototype.unshort = function () {
};


// return the bounding box of an absolute normalized path.
// normalized = without arc segments (A) and without reduced segments (S,T)
//
SvgPath.prototype.getBoundingBox = function () {
var bb = box();

if (this.segments.length === 0) {
return bb;
}

var P = this.copy().abs().unarc().unshort();

P.iterate(function (s, i, x, y) {
switch (s[0]) {
case 'H':
bb.addX(s[1]);
break;
case 'V':
bb.addY(s[1]);
break;
case 'M':
case 'L':
bb.addX(s[1]);
bb.addY(s[2]);
break;
case 'Q':
bb.addXQ([ x, s[1], s[3] ]);
bb.addYQ([ y, s[2], s[4] ]);
break;
case 'C':
bb.addXC([ x, s[1], s[3], s[5] ]);
bb.addYC([ y, s[2], s[4], s[6] ]);
break;
} // end switch
}, true); // end iterate

return bb;
};


// return a string that can be used as a viewBox for the path
//
SvgPath.prototype.toViewBoxString = function (pr) {
return this.getBoundingBox().toViewBoxString(pr);
};

// translate and scale the path to fit in a box
// controlled by the following parameters :
// - type:
// - fit(=none) : scale the path (aspect ratio is not preserved) to fit the box
// - meet (the default) : scale the path (aspect ratio is preserved) as much as possible
// to fit the entire path in the box
// - slice : scale the path (aspect ratio is preserved) as less as possible to cover the box
// - move : translate only (no scale) the path according to x???y??? parameter
// - position x(Min|Mid|Max)Y(Min|Mid|Max).
// example : .toBox('-10 10 300 400 meet xMidYMin')
//
SvgPath.prototype.toBox = function (parameters) {
this.matrix(this.getBoundingBox().matrixToBox(parameters));

return this;
};

module.exports = SvgPath;
Loading

0 comments on commit 619c7fc

Please sign in to comment.