1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-01 20:33:34 +02:00

bezier.js, as ES6 code

This commit is contained in:
Pomax
2020-08-07 15:47:52 -07:00
parent e49c314051
commit b13d8fe00d
29 changed files with 2234 additions and 4663 deletions

View File

@@ -21,13 +21,15 @@ drawBasics() {
translate(this.height, 0);
line({x:0, y:0}, {x:0, y:this.height});
line(0, 0, 0, this.height);
this.curve.drawSkeleton();
text(`Second interpolation, between each generated pair`, {x:5, y:15});
translate(this.height, 0);
line({x:0, y:0}, {x:0, y:this.height});
line(0, 0, 0, this.height);
this.curve.drawSkeleton();
text(`Curve points generated this way`, {x:5, y:15});
}
@@ -36,7 +38,7 @@ drawPointCurve() {
setStroke(`lightgrey`);
for(let i=1, e=50, p; i<=e; i++) {
p = this.curve.get(i/e);
circle(p, 1);
circle(p.x, p.y, 1);
}
}
@@ -57,12 +59,14 @@ setIterationColor(i) {
}
drawFirstInterpolation(p, i) {
p = p.map(v => new Vector(v));
let np2 = p[1].subtract(p[1].subtract(p[0]).scale(1 - i/100));
circle(np2, 5);
circle(np2.x, np2.y, 5);
text(`${i}%`, np2.add({x:10,y:0}));
let np3 = p[2].subtract(p[2].subtract(p[1]).scale(1 - i/100));
circle(np3, 5);
circle(np3.x, np3.y, 5);
text(`${i}%`, np3.add({x:-10,y:-15}));
return [np2, np3];
@@ -71,12 +75,12 @@ drawFirstInterpolation(p, i) {
drawSecondInterpolation(np2, np3, i) {
translate(this.height, 0);
line(np2, np3);
circle(np2, 5);
circle(np3, 5);
line(np2.x, np2.y, np3.x, np3.y);
circle(np2.x, np2.y, 5);
circle(np3.x, np3.y, 5);
let np4 = np3.subtract(np3.subtract(np2).scale(1 - i/100));
circle(np4, 2);
circle(np4.x, np4.y, 2);
text(`${i}%`, np4.add({x:10,y:10}));
return np4;
@@ -84,7 +88,7 @@ drawSecondInterpolation(np2, np3, i) {
drawOnCurve(np4, i) {
translate(this.height, 0);
circle(np4, 2);
circle(np4.x, np4.y, 2);
text(`ratio = ${i/100}`, np4.add({x:10,y:15}));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,964 +0,0 @@
/**
A javascript Bezier curve library by Pomax.
Based on http://pomax.github.io/bezierinfo
This code is MIT licensed.
**/
(function() {
"use strict";
// math-inlining.
var abs = Math.abs,
min = Math.min,
max = Math.max,
cos = Math.cos,
sin = Math.sin,
acos = Math.acos,
sqrt = Math.sqrt,
pi = Math.PI,
// a zero coordinate, which is surprisingly useful
ZERO = { x: 0, y: 0, z: 0 };
// quite needed
var utils = require("./utils.js");
// only used for outlines atm.
var PolyBezier = require("./poly-bezier.js");
/**
* Bezier curve constructor. The constructor argument can be one of three things:
*
* 1. array/4 of {x:..., y:..., z:...}, z optional
* 2. numerical array/8 ordered x1,y1,x2,y2,x3,y3,x4,y4
* 3. numerical array/12 ordered x1,y1,z1,x2,y2,z2,x3,y3,z3,x4,y4,z4
*
*/
var Bezier = function(coords) {
var args = coords && coords.forEach ? coords : [].slice.call(arguments);
var coordlen = false;
if (typeof args[0] === "object") {
coordlen = args.length;
var newargs = [];
args.forEach(function(point) {
["x", "y", "z"].forEach(function(d) {
if (typeof point[d] !== "undefined") {
newargs.push(point[d]);
}
});
});
args = newargs;
}
var higher = false;
var len = args.length;
if (coordlen) {
if (coordlen > 4) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
higher = true;
}
} else {
if (len !== 6 && len !== 8 && len !== 9 && len !== 12) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
}
}
var _3d =
(!higher && (len === 9 || len === 12)) ||
(coords && coords[0] && typeof coords[0].z !== "undefined");
this._3d = _3d;
var points = [];
for (var idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) {
var point = {
x: args[idx],
y: args[idx + 1]
};
if (_3d) {
point.z = args[idx + 2];
}
points.push(point);
}
this.order = points.length - 1;
this.points = points;
var dims = ["x", "y"];
if (_3d) dims.push("z");
this.dims = dims;
this.dimlen = dims.length;
(function(curve) {
var order = curve.order;
var points = curve.points;
var a = utils.align(points, { p1: points[0], p2: points[order] });
for (var i = 0; i < a.length; i++) {
if (abs(a[i].y) > 0.0001) {
curve._linear = false;
return;
}
}
curve._linear = true;
})(this);
this._t1 = 0;
this._t2 = 1;
this.update();
};
var svgToBeziers = require("./svg-to-beziers");
/**
* turn an svg <path> d attribute into a sequence of Bezier segments.
*/
Bezier.SVGtoBeziers = function(d) {
return svgToBeziers(Bezier, d);
};
function getABC(n, S, B, E, t) {
if (typeof t === "undefined") {
t = 0.5;
}
var u = utils.projectionratio(t, n),
um = 1 - u,
C = {
x: u * S.x + um * E.x,
y: u * S.y + um * E.y
},
s = utils.abcratio(t, n),
A = {
x: B.x + (B.x - C.x) / s,
y: B.y + (B.y - C.y) / s
};
return { A: A, B: B, C: C };
}
Bezier.quadraticFromPoints = function(p1, p2, p3, t) {
if (typeof t === "undefined") {
t = 0.5;
}
// shortcuts, although they're really dumb
if (t === 0) {
return new Bezier(p2, p2, p3);
}
if (t === 1) {
return new Bezier(p1, p2, p2);
}
// real fitting.
var abc = getABC(2, p1, p2, p3, t);
return new Bezier(p1, abc.A, p3);
};
Bezier.cubicFromPoints = function(S, B, E, t, d1) {
if (typeof t === "undefined") {
t = 0.5;
}
var abc = getABC(3, S, B, E, t);
if (typeof d1 === "undefined") {
d1 = utils.dist(B, abc.C);
}
var d2 = d1 * (1 - t) / t;
var selen = utils.dist(S, E),
lx = (E.x - S.x) / selen,
ly = (E.y - S.y) / selen,
bx1 = d1 * lx,
by1 = d1 * ly,
bx2 = d2 * lx,
by2 = d2 * ly;
// derivation of new hull coordinates
var e1 = { x: B.x - bx1, y: B.y - by1 },
e2 = { x: B.x + bx2, y: B.y + by2 },
A = abc.A,
v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) },
v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t },
nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t },
nc2 = {
x: E.x + (v2.x - E.x) / (1 - t),
y: E.y + (v2.y - E.y) / (1 - t)
};
// ...done
return new Bezier(S, nc1, nc2, E);
};
var getUtils = function() {
return utils;
};
Bezier.getUtils = getUtils;
Bezier.PolyBezier = PolyBezier;
Bezier.prototype = {
getUtils: getUtils,
valueOf: function() {
return this.toString();
},
toString: function() {
return utils.pointsToString(this.points);
},
toSVG: function(relative) {
if (this._3d) return false;
var p = this.points,
x = p[0].x,
y = p[0].y,
s = ["M", x, y, this.order === 2 ? "Q" : "C"];
for (var i = 1, last = p.length; i < last; i++) {
s.push(p[i].x);
s.push(p[i].y);
}
return s.join(" ");
},
setRatios: function(ratios) {
if (ratios.length !== this.points.length) {
throw new Error("incorrect number of ratio values");
}
this.ratios = ratios;
this._lut = []; // invalidate any precomputed LUT
},
verify: function() {
var print = this.coordDigest();
if (print !== this._print) {
this._print = print;
this.update();
}
},
coordDigest: function() {
return this.points.map(function(c,pos) {
return '' + pos + c.x + c.y + (c.z?c.z:0);
}).join('');
},
update: function(newprint) {
// invalidate any precomputed LUT
this._lut = [];
this.dpoints = utils.derive(this.points, this._3d);
this.computedirection();
},
computedirection: function() {
var points = this.points;
var angle = utils.angle(points[0], points[this.order], points[1]);
this.clockwise = angle > 0;
},
length: function() {
return utils.length(this.derivative.bind(this));
},
_lut: [],
getLUT: function(steps) {
this.verify();
steps = steps || 100;
if (this._lut.length === steps) {
return this._lut;
}
this._lut = [];
// We want a range from 0 to 1 inclusive, so
// we decrement and then use <= rather than <:
steps--;
for (var t = 0; t <= steps; t++) {
this._lut.push(this.compute(t / steps));
}
return this._lut;
},
on: function(point, error) {
error = error || 5;
var lut = this.getLUT(),
hits = [],
c,
t = 0;
for (var i = 0; i < lut.length; i++) {
c = lut[i];
if (utils.dist(c, point) < error) {
hits.push(c);
t += i / lut.length;
}
}
if (!hits.length) return false;
return (t /= hits.length);
},
project: function(point) {
// step 1: coarse check
var LUT = this.getLUT(),
l = LUT.length - 1,
closest = utils.closest(LUT, point),
mdist = closest.mdist,
mpos = closest.mpos;
// step 2: fine check
var ft,
t,
p,
d,
t1 = (mpos - 1) / l,
t2 = (mpos + 1) / l,
step = 0.1 / l;
mdist += 1;
for (t = t1, ft = t; t < t2 + step; t += step) {
p = this.compute(t);
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
ft = t;
}
}
p = this.compute(ft);
p.t = ft;
p.d = mdist;
return p;
},
get: function(t) {
return this.compute(t);
},
point: function(idx) {
return this.points[idx];
},
compute: function(t) {
if (this.ratios) return utils.computeWithRatios(t, this.points, this.ratios, this._3d);
return utils.compute(t, this.points, this._3d, this.ratios);
},
raise: function() {
var p = this.points,
np = [p[0]],
i,
k = p.length,
pi,
pim;
for (var i = 1; i < k; i++) {
pi = p[i];
pim = p[i - 1];
np[i] = {
x: (k - i) / k * pi.x + i / k * pim.x,
y: (k - i) / k * pi.y + i / k * pim.y
};
}
np[k] = p[k - 1];
return new Bezier(np);
},
derivative: function(t) {
var mt = 1 - t,
a,
b,
c = 0,
p = this.dpoints[0];
if (this.order === 2) {
p = [p[0], p[1], ZERO];
a = mt;
b = t;
}
if (this.order === 3) {
a = mt * mt;
b = mt * t * 2;
c = t * t;
}
var ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x,
y: a * p[0].y + b * p[1].y + c * p[2].y
};
if (this._3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z;
}
return ret;
},
curvature: function(t) {
return utils.curvature(t, this.points, this._3d);
},
inflections: function() {
return utils.inflections(this.points);
},
normal: function(t) {
return this._3d ? this.__normal3(t) : this.__normal2(t);
},
__normal2: function(t) {
var d = this.derivative(t);
var q = sqrt(d.x * d.x + d.y * d.y);
return { x: -d.y / q, y: d.x / q };
},
__normal3: function(t) {
// see http://stackoverflow.com/questions/25453159
var r1 = this.derivative(t),
r2 = this.derivative(t + 0.01),
q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z),
q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z);
r1.x /= q1;
r1.y /= q1;
r1.z /= q1;
r2.x /= q2;
r2.y /= q2;
r2.z /= q2;
// cross product
var c = {
x: r2.y * r1.z - r2.z * r1.y,
y: r2.z * r1.x - r2.x * r1.z,
z: r2.x * r1.y - r2.y * r1.x
};
var m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z);
c.x /= m;
c.y /= m;
c.z /= m;
// rotation matrix
var R = [
c.x * c.x,
c.x * c.y - c.z,
c.x * c.z + c.y,
c.x * c.y + c.z,
c.y * c.y,
c.y * c.z - c.x,
c.x * c.z - c.y,
c.y * c.z + c.x,
c.z * c.z
];
// normal vector:
var n = {
x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z,
y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z,
z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z
};
return n;
},
hull: function(t) {
var p = this.points,
_p = [],
pt,
q = [],
idx = 0,
i = 0,
l = 0;
q[idx++] = p[0];
q[idx++] = p[1];
q[idx++] = p[2];
if (this.order === 3) {
q[idx++] = p[3];
}
// we lerp between all points at each iteration, until we have 1 point left.
while (p.length > 1) {
_p = [];
for (i = 0, l = p.length - 1; i < l; i++) {
pt = utils.lerp(t, p[i], p[i + 1]);
q[idx++] = pt;
_p.push(pt);
}
p = _p;
}
return q;
},
split: function(t1, t2) {
// shortcuts
if (t1 === 0 && !!t2) {
return this.split(t2).left;
}
if (t2 === 1) {
return this.split(t1).right;
}
// no shortcut: use "de Casteljau" iteration.
var q = this.hull(t1);
var result = {
left:
this.order === 2
? new Bezier([q[0], q[3], q[5]])
: new Bezier([q[0], q[4], q[7], q[9]]),
right:
this.order === 2
? new Bezier([q[5], q[4], q[2]])
: new Bezier([q[9], q[8], q[6], q[3]]),
span: q
};
// make sure we bind _t1/_t2 information!
result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2);
result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2);
// if we have no t2, we're done
if (!t2) {
return result;
}
// if we have a t2, split again:
t2 = utils.map(t2, t1, 1, 0, 1);
var subsplit = result.right.split(t2);
return subsplit.left;
},
extrema: function() {
var dims = this.dims,
result = {},
roots = [],
p,
mfn;
dims.forEach(
function(dim) {
mfn = function(v) {
return v[dim];
};
p = this.dpoints[0].map(mfn);
result[dim] = utils.droots(p);
if (this.order === 3) {
p = this.dpoints[1].map(mfn);
result[dim] = result[dim].concat(utils.droots(p));
}
result[dim] = result[dim].filter(function(t) {
return t >= 0 && t <= 1;
});
roots = roots.concat(result[dim].sort(utils.numberSort));
}.bind(this)
);
roots = roots.sort(utils.numberSort).filter(function(v, idx) {
return roots.indexOf(v) === idx;
});
result.values = roots;
return result;
},
bbox: function() {
var extrema = this.extrema(),
result = {};
this.dims.forEach(
function(d) {
result[d] = utils.getminmax(this, d, extrema[d]);
}.bind(this)
);
return result;
},
overlaps: function(curve) {
var lbbox = this.bbox(),
tbbox = curve.bbox();
return utils.bboxoverlap(lbbox, tbbox);
},
offset: function(t, d) {
if (typeof d !== "undefined") {
var c = this.get(t);
var n = this.normal(t);
var ret = {
c: c,
n: n,
x: c.x + n.x * d,
y: c.y + n.y * d
};
if (this._3d) {
ret.z = c.z + n.z * d;
}
return ret;
}
if (this._linear) {
var nv = this.normal(0);
var coords = this.points.map(function(p) {
var ret = {
x: p.x + t * nv.x,
y: p.y + t * nv.y
};
if (p.z && n.z) {
ret.z = p.z + t * nv.z;
}
return ret;
});
return [new Bezier(coords)];
}
var reduced = this.reduce();
return reduced.map(function(s) {
if (s._linear) {
return s.offset(t)[0];
}
return s.scale(t);
});
},
simple: function() {
if (this.order === 3) {
var a1 = utils.angle(this.points[0], this.points[3], this.points[1]);
var a2 = utils.angle(this.points[0], this.points[3], this.points[2]);
if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false;
}
var n1 = this.normal(0);
var n2 = this.normal(1);
var s = n1.x * n2.x + n1.y * n2.y;
if (this._3d) {
s += n1.z * n2.z;
}
var angle = abs(acos(s));
return angle < pi / 3;
},
reduce: function() {
var i,
t1 = 0,
t2 = 0,
step = 0.01,
segment,
pass1 = [],
pass2 = [];
// first pass: split on extrema
var extrema = this.extrema().values;
if (extrema.indexOf(0) === -1) {
extrema = [0].concat(extrema);
}
if (extrema.indexOf(1) === -1) {
extrema.push(1);
}
for (t1 = extrema[0], i = 1; i < extrema.length; i++) {
t2 = extrema[i];
segment = this.split(t1, t2);
segment._t1 = t1;
segment._t2 = t2;
pass1.push(segment);
t1 = t2;
}
// second pass: further reduce these segments to simple segments
pass1.forEach(function(p1) {
t1 = 0;
t2 = 0;
while (t2 <= 1) {
for (t2 = t1 + step; t2 <= 1 + step; t2 += step) {
segment = p1.split(t1, t2);
if (!segment.simple()) {
t2 -= step;
if (abs(t1 - t2) < step) {
// we can never form a reduction
return [];
}
segment = p1.split(t1, t2);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2);
pass2.push(segment);
t1 = t2;
break;
}
}
}
if (t1 < 1) {
segment = p1.split(t1, 1);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = p1._t2;
pass2.push(segment);
}
});
return pass2;
},
scale: function(d) {
var order = this.order;
var distanceFn = false;
if (typeof d === "function") {
distanceFn = d;
}
if (distanceFn && order === 2) {
return this.raise().scale(distanceFn);
}
// TODO: add special handling for degenerate (=linear) curves.
var clockwise = this.clockwise;
var r1 = distanceFn ? distanceFn(0) : d;
var r2 = distanceFn ? distanceFn(1) : d;
var v = [this.offset(0, 10), this.offset(1, 10)];
var o = utils.lli4(v[0], v[0].c, v[1], v[1].c);
if (!o) {
throw new Error("cannot scale this curve. Try reducing it first.");
}
// move all points by distance 'd' wrt the origin 'o'
var points = this.points,
np = [];
// move end points by fixed distance along normal.
[0, 1].forEach(
function(t) {
var p = (np[t * order] = utils.copy(points[t * order]));
p.x += (t ? r2 : r1) * v[t].n.x;
p.y += (t ? r2 : r1) * v[t].n.y;
}.bind(this)
);
if (!distanceFn) {
// move control points to lie on the intersection of the offset
// derivative vector, and the origin-through-control vector
[0, 1].forEach(
function(t) {
if (this.order === 2 && !!t) return;
var p = np[t * order];
var d = this.derivative(t);
var p2 = { x: p.x + d.x, y: p.y + d.y };
np[t + 1] = utils.lli4(p, p2, o, points[t + 1]);
}.bind(this)
);
return new Bezier(np);
}
// move control points by "however much necessary to
// ensure the correct tangent to endpoint".
[0, 1].forEach(
function(t) {
if (this.order === 2 && !!t) return;
var p = points[t + 1];
var ov = {
x: p.x - o.x,
y: p.y - o.y
};
var rc = distanceFn ? distanceFn((t + 1) / order) : d;
if (distanceFn && !clockwise) rc = -rc;
var m = sqrt(ov.x * ov.x + ov.y * ov.y);
ov.x /= m;
ov.y /= m;
np[t + 1] = {
x: p.x + rc * ov.x,
y: p.y + rc * ov.y
};
}.bind(this)
);
return new Bezier(np);
},
outline: function(d1, d2, d3, d4) {
d2 = typeof d2 === "undefined" ? d1 : d2;
var reduced = this.reduce(),
len = reduced.length,
fcurves = [],
bcurves = [],
p,
alen = 0,
tlen = this.length();
var graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined";
function linearDistanceFunction(s, e, tlen, alen, slen) {
return function(v) {
var f1 = alen / tlen,
f2 = (alen + slen) / tlen,
d = e - s;
return utils.map(v, 0, 1, s + f1 * d, s + f2 * d);
};
}
// form curve oulines
reduced.forEach(function(segment) {
slen = segment.length();
if (graduated) {
fcurves.push(
segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen))
);
bcurves.push(
segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen))
);
} else {
fcurves.push(segment.scale(d1));
bcurves.push(segment.scale(-d2));
}
alen += slen;
});
// reverse the "return" outline
bcurves = bcurves
.map(function(s) {
p = s.points;
if (p[3]) {
s.points = [p[3], p[2], p[1], p[0]];
} else {
s.points = [p[2], p[1], p[0]];
}
return s;
})
.reverse();
// form the endcaps as lines
var fs = fcurves[0].points[0],
fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1],
bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1],
be = bcurves[0].points[0],
ls = utils.makeline(bs, fs),
le = utils.makeline(fe, be),
segments = [ls]
.concat(fcurves)
.concat([le])
.concat(bcurves),
slen = segments.length;
return new PolyBezier(segments);
},
outlineshapes: function(d1, d2, curveIntersectionThreshold) {
d2 = d2 || d1;
var outline = this.outline(d1, d2).curves;
var shapes = [];
for (var i = 1, len = outline.length; i < len / 2; i++) {
var shape = utils.makeshape(
outline[i],
outline[len - i],
curveIntersectionThreshold
);
shape.startcap.virtual = i > 1;
shape.endcap.virtual = i < len / 2 - 1;
shapes.push(shape);
}
return shapes;
},
intersects: function(curve, curveIntersectionThreshold) {
if (!curve) return this.selfintersects(curveIntersectionThreshold);
if (curve.p1 && curve.p2) {
return this.lineIntersects(curve);
}
if (curve instanceof Bezier) {
curve = curve.reduce();
}
return this.curveintersects(
this.reduce(),
curve,
curveIntersectionThreshold
);
},
lineIntersects: function(line) {
var mx = min(line.p1.x, line.p2.x),
my = min(line.p1.y, line.p2.y),
MX = max(line.p1.x, line.p2.x),
MY = max(line.p1.y, line.p2.y),
self = this;
return utils.roots(this.points, line).filter(function(t) {
var p = self.get(t);
return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY);
});
},
selfintersects: function(curveIntersectionThreshold) {
var reduced = this.reduce();
// "simple" curves cannot intersect with their direct
// neighbour, so for each segment X we check whether
// it intersects [0:x-2][x+2:last].
var i,
len = reduced.length - 2,
results = [],
result,
left,
right;
for (i = 0; i < len; i++) {
left = reduced.slice(i, i + 1);
right = reduced.slice(i + 2);
result = this.curveintersects(left, right, curveIntersectionThreshold);
results = results.concat(result);
}
return results;
},
curveintersects: function(c1, c2, curveIntersectionThreshold) {
var pairs = [];
// step 1: pair off any overlapping segments
c1.forEach(function(l) {
c2.forEach(function(r) {
if (l.overlaps(r)) {
pairs.push({ left: l, right: r });
}
});
});
// step 2: for each pairing, run through the convergence algorithm.
var intersections = [];
pairs.forEach(function(pair) {
var result = utils.pairiteration(
pair.left,
pair.right,
curveIntersectionThreshold
);
if (result.length > 0) {
intersections = intersections.concat(result);
}
});
return intersections;
},
arcs: function(errorThreshold) {
errorThreshold = errorThreshold || 0.5;
var circles = [];
return this._iterate(errorThreshold, circles);
},
_error: function(pc, np1, s, e) {
var q = (e - s) / 4,
c1 = this.get(s + q),
c2 = this.get(e - q),
ref = utils.dist(pc, np1),
d1 = utils.dist(pc, c1),
d2 = utils.dist(pc, c2);
return abs(d1 - ref) + abs(d2 - ref);
},
_iterate: function(errorThreshold, circles) {
var t_s = 0,
t_e = 1,
safety;
// we do a binary search to find the "good `t` closest to no-longer-good"
do {
safety = 0;
// step 1: start with the maximum possible arc
t_e = 1;
// points:
var np1 = this.get(t_s),
np2,
np3,
arc,
prev_arc;
// booleans:
var curr_good = false,
prev_good = false,
done;
// numbers:
var t_m = t_e,
prev_e = 1,
step = 0;
// step 2: find the best possible arc
do {
prev_good = curr_good;
prev_arc = arc;
t_m = (t_s + t_e) / 2;
step++;
np2 = this.get(t_m);
np3 = this.get(t_e);
arc = utils.getccenter(np1, np2, np3);
//also save the t values
arc.interval = {
start: t_s,
end: t_e
};
var error = this._error(arc, np1, t_s, t_e);
curr_good = error <= errorThreshold;
done = prev_good && !curr_good;
if (!done) prev_e = t_e;
// this arc is fine: we can move 'e' up to see if we can find a wider arc
if (curr_good) {
// if e is already at max, then we're done for this arc.
if (t_e >= 1) {
// make sure we cap at t=1
arc.interval.end = prev_e = 1;
prev_arc = arc;
// if we capped the arc segment to t=1 we also need to make sure that
// the arc's end angle is correct with respect to the bezier end point.
if (t_e > 1) {
var d = {
x: arc.x + arc.r * cos(arc.e),
y: arc.y + arc.r * sin(arc.e)
};
arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1));
}
break;
}
// if not, move it up by half the iteration distance
t_e = t_e + (t_e - t_s) / 2;
} else {
// this is a bad arc: we need to move 'e' down to find a good arc
t_e = t_m;
}
} while (!done && safety++ < 100);
if (safety >= 100) {
break;
}
// console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e);
prev_arc = prev_arc ? prev_arc : arc;
circles.push(prev_arc);
t_s = prev_e;
} while (t_e < 1);
return circles;
}
};
module.exports = Bezier;
})();

View File

@@ -1,964 +0,0 @@
/**
A javascript Bezier curve library by Pomax.
Based on http://pomax.github.io/bezierinfo
This code is MIT licensed.
**/
(function() {
"use strict";
// math-inlining.
var abs = Math.abs,
min = Math.min,
max = Math.max,
cos = Math.cos,
sin = Math.sin,
acos = Math.acos,
sqrt = Math.sqrt,
pi = Math.PI,
// a zero coordinate, which is surprisingly useful
ZERO = { x: 0, y: 0, z: 0 };
// quite needed
var utils = require("./utils.js");
// only used for outlines atm.
var PolyBezier = require("./poly-bezier.js");
/**
* Bezier curve constructor. The constructor argument can be one of three things:
*
* 1. array/4 of {x:..., y:..., z:...}, z optional
* 2. numerical array/8 ordered x1,y1,x2,y2,x3,y3,x4,y4
* 3. numerical array/12 ordered x1,y1,z1,x2,y2,z2,x3,y3,z3,x4,y4,z4
*
*/
var Bezier = function(coords) {
var args = coords && coords.forEach ? coords : [].slice.call(arguments);
var coordlen = false;
if (typeof args[0] === "object") {
coordlen = args.length;
var newargs = [];
args.forEach(function(point) {
["x", "y", "z"].forEach(function(d) {
if (typeof point[d] !== "undefined") {
newargs.push(point[d]);
}
});
});
args = newargs;
}
var higher = false;
var len = args.length;
if (coordlen) {
if (coordlen > 4) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
higher = true;
}
} else {
if (len !== 6 && len !== 8 && len !== 9 && len !== 12) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
}
}
var _3d =
(!higher && (len === 9 || len === 12)) ||
(coords && coords[0] && typeof coords[0].z !== "undefined");
this._3d = _3d;
var points = [];
for (var idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) {
var point = {
x: args[idx],
y: args[idx + 1]
};
if (_3d) {
point.z = args[idx + 2];
}
points.push(point);
}
this.order = points.length - 1;
this.points = points;
var dims = ["x", "y"];
if (_3d) dims.push("z");
this.dims = dims;
this.dimlen = dims.length;
(function(curve) {
var order = curve.order;
var points = curve.points;
var a = utils.align(points, { p1: points[0], p2: points[order] });
for (var i = 0; i < a.length; i++) {
if (abs(a[i].y) > 0.0001) {
curve._linear = false;
return;
}
}
curve._linear = true;
})(this);
this._t1 = 0;
this._t2 = 1;
this.update();
};
var svgToBeziers = require("./svg-to-beziers");
/**
* turn an svg <path> d attribute into a sequence of Bezier segments.
*/
Bezier.SVGtoBeziers = function(d) {
return svgToBeziers(Bezier, d);
};
function getABC(n, S, B, E, t) {
if (typeof t === "undefined") {
t = 0.5;
}
var u = utils.projectionratio(t, n),
um = 1 - u,
C = {
x: u * S.x + um * E.x,
y: u * S.y + um * E.y
},
s = utils.abcratio(t, n),
A = {
x: B.x + (B.x - C.x) / s,
y: B.y + (B.y - C.y) / s
};
return { A: A, B: B, C: C };
}
Bezier.quadraticFromPoints = function(p1, p2, p3, t) {
if (typeof t === "undefined") {
t = 0.5;
}
// shortcuts, although they're really dumb
if (t === 0) {
return new Bezier(p2, p2, p3);
}
if (t === 1) {
return new Bezier(p1, p2, p2);
}
// real fitting.
var abc = getABC(2, p1, p2, p3, t);
return new Bezier(p1, abc.A, p3);
};
Bezier.cubicFromPoints = function(S, B, E, t, d1) {
if (typeof t === "undefined") {
t = 0.5;
}
var abc = getABC(3, S, B, E, t);
if (typeof d1 === "undefined") {
d1 = utils.dist(B, abc.C);
}
var d2 = d1 * (1 - t) / t;
var selen = utils.dist(S, E),
lx = (E.x - S.x) / selen,
ly = (E.y - S.y) / selen,
bx1 = d1 * lx,
by1 = d1 * ly,
bx2 = d2 * lx,
by2 = d2 * ly;
// derivation of new hull coordinates
var e1 = { x: B.x - bx1, y: B.y - by1 },
e2 = { x: B.x + bx2, y: B.y + by2 },
A = abc.A,
v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) },
v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t },
nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t },
nc2 = {
x: E.x + (v2.x - E.x) / (1 - t),
y: E.y + (v2.y - E.y) / (1 - t)
};
// ...done
return new Bezier(S, nc1, nc2, E);
};
var getUtils = function() {
return utils;
};
Bezier.getUtils = getUtils;
Bezier.PolyBezier = PolyBezier;
Bezier.prototype = {
getUtils: getUtils,
valueOf: function() {
return this.toString();
},
toString: function() {
return utils.pointsToString(this.points);
},
toSVG: function(relative) {
if (this._3d) return false;
var p = this.points,
x = p[0].x,
y = p[0].y,
s = ["M", x, y, this.order === 2 ? "Q" : "C"];
for (var i = 1, last = p.length; i < last; i++) {
s.push(p[i].x);
s.push(p[i].y);
}
return s.join(" ");
},
setRatios: function(ratios) {
if (ratios.length !== this.points.length) {
throw new Error("incorrect number of ratio values");
}
this.ratios = ratios;
this._lut = []; // invalidate any precomputed LUT
},
verify: function() {
var print = this.coordDigest();
if (print !== this._print) {
this._print = print;
this.update();
}
},
coordDigest: function() {
return this.points.map(function(c,pos) {
return '' + pos + c.x + c.y + (c.z?c.z:0);
}).join('');
},
update: function(newprint) {
// invalidate any precomputed LUT
this._lut = [];
this.dpoints = utils.derive(this.points, this._3d);
this.computedirection();
},
computedirection: function() {
var points = this.points;
var angle = utils.angle(points[0], points[this.order], points[1]);
this.clockwise = angle > 0;
},
length: function() {
return utils.length(this.derivative.bind(this));
},
_lut: [],
getLUT: function(steps) {
this.verify();
steps = steps || 100;
if (this._lut.length === steps) {
return this._lut;
}
this._lut = [];
// We want a range from 0 to 1 inclusive, so
// we decrement and then use <= rather than <:
steps--;
for (var t = 0; t <= steps; t++) {
this._lut.push(this.compute(t / steps));
}
return this._lut;
},
on: function(point, error) {
error = error || 5;
var lut = this.getLUT(),
hits = [],
c,
t = 0;
for (var i = 0; i < lut.length; i++) {
c = lut[i];
if (utils.dist(c, point) < error) {
hits.push(c);
t += i / lut.length;
}
}
if (!hits.length) return false;
return (t /= hits.length);
},
project: function(point) {
// step 1: coarse check
var LUT = this.getLUT(),
l = LUT.length - 1,
closest = utils.closest(LUT, point),
mdist = closest.mdist,
mpos = closest.mpos;
// step 2: fine check
var ft,
t,
p,
d,
t1 = (mpos - 1) / l,
t2 = (mpos + 1) / l,
step = 0.1 / l;
mdist += 1;
for (t = t1, ft = t; t < t2 + step; t += step) {
p = this.compute(t);
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
ft = t;
}
}
p = this.compute(ft);
p.t = ft;
p.d = mdist;
return p;
},
get: function(t) {
return this.compute(t);
},
point: function(idx) {
return this.points[idx];
},
compute: function(t) {
if (this.ratios) return utils.computeWithRatios(t, this.points, this.ratios, this._3d);
return utils.compute(t, this.points, this._3d, this.ratios);
},
raise: function() {
var p = this.points,
np = [p[0]],
i,
k = p.length,
pi,
pim;
for (var i = 1; i < k; i++) {
pi = p[i];
pim = p[i - 1];
np[i] = {
x: (k - i) / k * pi.x + i / k * pim.x,
y: (k - i) / k * pi.y + i / k * pim.y
};
}
np[k] = p[k - 1];
return new Bezier(np);
},
derivative: function(t) {
var mt = 1 - t,
a,
b,
c = 0,
p = this.dpoints[0];
if (this.order === 2) {
p = [p[0], p[1], ZERO];
a = mt;
b = t;
}
if (this.order === 3) {
a = mt * mt;
b = mt * t * 2;
c = t * t;
}
var ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x,
y: a * p[0].y + b * p[1].y + c * p[2].y
};
if (this._3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z;
}
return ret;
},
curvature: function(t) {
return utils.curvature(t, this.points, this._3d);
},
inflections: function() {
return utils.inflections(this.points);
},
normal: function(t) {
return this._3d ? this.__normal3(t) : this.__normal2(t);
},
__normal2: function(t) {
var d = this.derivative(t);
var q = sqrt(d.x * d.x + d.y * d.y);
return { x: -d.y / q, y: d.x / q };
},
__normal3: function(t) {
// see http://stackoverflow.com/questions/25453159
var r1 = this.derivative(t),
r2 = this.derivative(t + 0.01),
q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z),
q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z);
r1.x /= q1;
r1.y /= q1;
r1.z /= q1;
r2.x /= q2;
r2.y /= q2;
r2.z /= q2;
// cross product
var c = {
x: r2.y * r1.z - r2.z * r1.y,
y: r2.z * r1.x - r2.x * r1.z,
z: r2.x * r1.y - r2.y * r1.x
};
var m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z);
c.x /= m;
c.y /= m;
c.z /= m;
// rotation matrix
var R = [
c.x * c.x,
c.x * c.y - c.z,
c.x * c.z + c.y,
c.x * c.y + c.z,
c.y * c.y,
c.y * c.z - c.x,
c.x * c.z - c.y,
c.y * c.z + c.x,
c.z * c.z
];
// normal vector:
var n = {
x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z,
y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z,
z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z
};
return n;
},
hull: function(t) {
var p = this.points,
_p = [],
pt,
q = [],
idx = 0,
i = 0,
l = 0;
q[idx++] = p[0];
q[idx++] = p[1];
q[idx++] = p[2];
if (this.order === 3) {
q[idx++] = p[3];
}
// we lerp between all points at each iteration, until we have 1 point left.
while (p.length > 1) {
_p = [];
for (i = 0, l = p.length - 1; i < l; i++) {
pt = utils.lerp(t, p[i], p[i + 1]);
q[idx++] = pt;
_p.push(pt);
}
p = _p;
}
return q;
},
split: function(t1, t2) {
// shortcuts
if (t1 === 0 && !!t2) {
return this.split(t2).left;
}
if (t2 === 1) {
return this.split(t1).right;
}
// no shortcut: use "de Casteljau" iteration.
var q = this.hull(t1);
var result = {
left:
this.order === 2
? new Bezier([q[0], q[3], q[5]])
: new Bezier([q[0], q[4], q[7], q[9]]),
right:
this.order === 2
? new Bezier([q[5], q[4], q[2]])
: new Bezier([q[9], q[8], q[6], q[3]]),
span: q
};
// make sure we bind _t1/_t2 information!
result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2);
result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2);
// if we have no t2, we're done
if (!t2) {
return result;
}
// if we have a t2, split again:
t2 = utils.map(t2, t1, 1, 0, 1);
var subsplit = result.right.split(t2);
return subsplit.left;
},
extrema: function() {
var dims = this.dims,
result = {},
roots = [],
p,
mfn;
dims.forEach(
function(dim) {
mfn = function(v) {
return v[dim];
};
p = this.dpoints[0].map(mfn);
result[dim] = utils.droots(p);
if (this.order === 3) {
p = this.dpoints[1].map(mfn);
result[dim] = result[dim].concat(utils.droots(p));
}
result[dim] = result[dim].filter(function(t) {
return t >= 0 && t <= 1;
});
roots = roots.concat(result[dim].sort(utils.numberSort));
}.bind(this)
);
roots = roots.sort(utils.numberSort).filter(function(v, idx) {
return roots.indexOf(v) === idx;
});
result.values = roots;
return result;
},
bbox: function() {
var extrema = this.extrema(),
result = {};
this.dims.forEach(
function(d) {
result[d] = utils.getminmax(this, d, extrema[d]);
}.bind(this)
);
return result;
},
overlaps: function(curve) {
var lbbox = this.bbox(),
tbbox = curve.bbox();
return utils.bboxoverlap(lbbox, tbbox);
},
offset: function(t, d) {
if (typeof d !== "undefined") {
var c = this.get(t);
var n = this.normal(t);
var ret = {
c: c,
n: n,
x: c.x + n.x * d,
y: c.y + n.y * d
};
if (this._3d) {
ret.z = c.z + n.z * d;
}
return ret;
}
if (this._linear) {
var nv = this.normal(0);
var coords = this.points.map(function(p) {
var ret = {
x: p.x + t * nv.x,
y: p.y + t * nv.y
};
if (p.z && n.z) {
ret.z = p.z + t * nv.z;
}
return ret;
});
return [new Bezier(coords)];
}
var reduced = this.reduce();
return reduced.map(function(s) {
if (s._linear) {
return s.offset(t)[0];
}
return s.scale(t);
});
},
simple: function() {
if (this.order === 3) {
var a1 = utils.angle(this.points[0], this.points[3], this.points[1]);
var a2 = utils.angle(this.points[0], this.points[3], this.points[2]);
if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false;
}
var n1 = this.normal(0);
var n2 = this.normal(1);
var s = n1.x * n2.x + n1.y * n2.y;
if (this._3d) {
s += n1.z * n2.z;
}
var angle = abs(acos(s));
return angle < pi / 3;
},
reduce: function() {
var i,
t1 = 0,
t2 = 0,
step = 0.01,
segment,
pass1 = [],
pass2 = [];
// first pass: split on extrema
var extrema = this.extrema().values;
if (extrema.indexOf(0) === -1) {
extrema = [0].concat(extrema);
}
if (extrema.indexOf(1) === -1) {
extrema.push(1);
}
for (t1 = extrema[0], i = 1; i < extrema.length; i++) {
t2 = extrema[i];
segment = this.split(t1, t2);
segment._t1 = t1;
segment._t2 = t2;
pass1.push(segment);
t1 = t2;
}
// second pass: further reduce these segments to simple segments
pass1.forEach(function(p1) {
t1 = 0;
t2 = 0;
while (t2 <= 1) {
for (t2 = t1 + step; t2 <= 1 + step; t2 += step) {
segment = p1.split(t1, t2);
if (!segment.simple()) {
t2 -= step;
if (abs(t1 - t2) < step) {
// we can never form a reduction
return [];
}
segment = p1.split(t1, t2);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2);
pass2.push(segment);
t1 = t2;
break;
}
}
}
if (t1 < 1) {
segment = p1.split(t1, 1);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = p1._t2;
pass2.push(segment);
}
});
return pass2;
},
scale: function(d) {
var order = this.order;
var distanceFn = false;
if (typeof d === "function") {
distanceFn = d;
}
if (distanceFn && order === 2) {
return this.raise().scale(distanceFn);
}
// TODO: add special handling for degenerate (=linear) curves.
var clockwise = this.clockwise;
var r1 = distanceFn ? distanceFn(0) : d;
var r2 = distanceFn ? distanceFn(1) : d;
var v = [this.offset(0, 10), this.offset(1, 10)];
var o = utils.lli4(v[0], v[0].c, v[1], v[1].c);
if (!o) {
throw new Error("cannot scale this curve. Try reducing it first.");
}
// move all points by distance 'd' wrt the origin 'o'
var points = this.points,
np = [];
// move end points by fixed distance along normal.
[0, 1].forEach(
function(t) {
var p = (np[t * order] = utils.copy(points[t * order]));
p.x += (t ? r2 : r1) * v[t].n.x;
p.y += (t ? r2 : r1) * v[t].n.y;
}.bind(this)
);
if (!distanceFn) {
// move control points to lie on the intersection of the offset
// derivative vector, and the origin-through-control vector
[0, 1].forEach(
function(t) {
if (this.order === 2 && !!t) return;
var p = np[t * order];
var d = this.derivative(t);
var p2 = { x: p.x + d.x, y: p.y + d.y };
np[t + 1] = utils.lli4(p, p2, o, points[t + 1]);
}.bind(this)
);
return new Bezier(np);
}
// move control points by "however much necessary to
// ensure the correct tangent to endpoint".
[0, 1].forEach(
function(t) {
if (this.order === 2 && !!t) return;
var p = points[t + 1];
var ov = {
x: p.x - o.x,
y: p.y - o.y
};
var rc = distanceFn ? distanceFn((t + 1) / order) : d;
if (distanceFn && !clockwise) rc = -rc;
var m = sqrt(ov.x * ov.x + ov.y * ov.y);
ov.x /= m;
ov.y /= m;
np[t + 1] = {
x: p.x + rc * ov.x,
y: p.y + rc * ov.y
};
}.bind(this)
);
return new Bezier(np);
},
outline: function(d1, d2, d3, d4) {
d2 = typeof d2 === "undefined" ? d1 : d2;
var reduced = this.reduce(),
len = reduced.length,
fcurves = [],
bcurves = [],
p,
alen = 0,
tlen = this.length();
var graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined";
function linearDistanceFunction(s, e, tlen, alen, slen) {
return function(v) {
var f1 = alen / tlen,
f2 = (alen + slen) / tlen,
d = e - s;
return utils.map(v, 0, 1, s + f1 * d, s + f2 * d);
};
}
// form curve oulines
reduced.forEach(function(segment) {
slen = segment.length();
if (graduated) {
fcurves.push(
segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen))
);
bcurves.push(
segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen))
);
} else {
fcurves.push(segment.scale(d1));
bcurves.push(segment.scale(-d2));
}
alen += slen;
});
// reverse the "return" outline
bcurves = bcurves
.map(function(s) {
p = s.points;
if (p[3]) {
s.points = [p[3], p[2], p[1], p[0]];
} else {
s.points = [p[2], p[1], p[0]];
}
return s;
})
.reverse();
// form the endcaps as lines
var fs = fcurves[0].points[0],
fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1],
bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1],
be = bcurves[0].points[0],
ls = utils.makeline(bs, fs),
le = utils.makeline(fe, be),
segments = [ls]
.concat(fcurves)
.concat([le])
.concat(bcurves),
slen = segments.length;
return new PolyBezier(segments);
},
outlineshapes: function(d1, d2, curveIntersectionThreshold) {
d2 = d2 || d1;
var outline = this.outline(d1, d2).curves;
var shapes = [];
for (var i = 1, len = outline.length; i < len / 2; i++) {
var shape = utils.makeshape(
outline[i],
outline[len - i],
curveIntersectionThreshold
);
shape.startcap.virtual = i > 1;
shape.endcap.virtual = i < len / 2 - 1;
shapes.push(shape);
}
return shapes;
},
intersects: function(curve, curveIntersectionThreshold) {
if (!curve) return this.selfintersects(curveIntersectionThreshold);
if (curve.p1 && curve.p2) {
return this.lineIntersects(curve);
}
if (curve instanceof Bezier) {
curve = curve.reduce();
}
return this.curveintersects(
this.reduce(),
curve,
curveIntersectionThreshold
);
},
lineIntersects: function(line) {
var mx = min(line.p1.x, line.p2.x),
my = min(line.p1.y, line.p2.y),
MX = max(line.p1.x, line.p2.x),
MY = max(line.p1.y, line.p2.y),
self = this;
return utils.roots(this.points, line).filter(function(t) {
var p = self.get(t);
return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY);
});
},
selfintersects: function(curveIntersectionThreshold) {
var reduced = this.reduce();
// "simple" curves cannot intersect with their direct
// neighbour, so for each segment X we check whether
// it intersects [0:x-2][x+2:last].
var i,
len = reduced.length - 2,
results = [],
result,
left,
right;
for (i = 0; i < len; i++) {
left = reduced.slice(i, i + 1);
right = reduced.slice(i + 2);
result = this.curveintersects(left, right, curveIntersectionThreshold);
results = results.concat(result);
}
return results;
},
curveintersects: function(c1, c2, curveIntersectionThreshold) {
var pairs = [];
// step 1: pair off any overlapping segments
c1.forEach(function(l) {
c2.forEach(function(r) {
if (l.overlaps(r)) {
pairs.push({ left: l, right: r });
}
});
});
// step 2: for each pairing, run through the convergence algorithm.
var intersections = [];
pairs.forEach(function(pair) {
var result = utils.pairiteration(
pair.left,
pair.right,
curveIntersectionThreshold
);
if (result.length > 0) {
intersections = intersections.concat(result);
}
});
return intersections;
},
arcs: function(errorThreshold) {
errorThreshold = errorThreshold || 0.5;
var circles = [];
return this._iterate(errorThreshold, circles);
},
_error: function(pc, np1, s, e) {
var q = (e - s) / 4,
c1 = this.get(s + q),
c2 = this.get(e - q),
ref = utils.dist(pc, np1),
d1 = utils.dist(pc, c1),
d2 = utils.dist(pc, c2);
return abs(d1 - ref) + abs(d2 - ref);
},
_iterate: function(errorThreshold, circles) {
var t_s = 0,
t_e = 1,
safety;
// we do a binary search to find the "good `t` closest to no-longer-good"
do {
safety = 0;
// step 1: start with the maximum possible arc
t_e = 1;
// points:
var np1 = this.get(t_s),
np2,
np3,
arc,
prev_arc;
// booleans:
var curr_good = false,
prev_good = false,
done;
// numbers:
var t_m = t_e,
prev_e = 1,
step = 0;
// step 2: find the best possible arc
do {
prev_good = curr_good;
prev_arc = arc;
t_m = (t_s + t_e) / 2;
step++;
np2 = this.get(t_m);
np3 = this.get(t_e);
arc = utils.getccenter(np1, np2, np3);
//also save the t values
arc.interval = {
start: t_s,
end: t_e
};
var error = this._error(arc, np1, t_s, t_e);
curr_good = error <= errorThreshold;
done = prev_good && !curr_good;
if (!done) prev_e = t_e;
// this arc is fine: we can move 'e' up to see if we can find a wider arc
if (curr_good) {
// if e is already at max, then we're done for this arc.
if (t_e >= 1) {
// make sure we cap at t=1
arc.interval.end = prev_e = 1;
prev_arc = arc;
// if we capped the arc segment to t=1 we also need to make sure that
// the arc's end angle is correct with respect to the bezier end point.
if (t_e > 1) {
var d = {
x: arc.x + arc.r * cos(arc.e),
y: arc.y + arc.r * sin(arc.e)
};
arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1));
}
break;
}
// if not, move it up by half the iteration distance
t_e = t_e + (t_e - t_s) / 2;
} else {
// this is a bad arc: we need to move 'e' down to find a good arc
t_e = t_m;
}
} while (!done && safety++ < 100);
if (safety >= 100) {
break;
}
// console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e);
prev_arc = prev_arc ? prev_arc : arc;
circles.push(prev_arc);
t_s = prev_e;
} while (t_e < 1);
return circles;
}
};
module.exports = Bezier;
})();

View File

@@ -1,197 +0,0 @@
/**
* Normalise an SVG path to absolute coordinates
* and full commands, rather than relative coordinates
* and/or shortcut commands.
*/
function normalizePath(d) {
// preprocess "d" so that we have spaces between values
d = d
.replace(/,/g, " ") // replace commas with spaces
.replace(/-/g, " - ") // add spacing around minus signs
.replace(/-\s+/g, "-") // remove spacing to the right of minus signs.
.replace(/([a-zA-Z])/g, " $1 ");
// set up the variables used in this function
var instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"),
instructionLength = instructions.length,
i,
instruction,
op,
lop,
args = [],
alen,
a,
sx = 0,
sy = 0,
x = 0,
y = 0,
cx = 0,
cy = 0,
cx2 = 0,
cy2 = 0,
normalized = "";
// we run through the instruction list starting at 1, not 0,
// because we split up "|M x y ...." so the first element will
// always be an empty string. By design.
for (i = 1; i < instructionLength; i++) {
// which instruction is this?
instruction = instructions[i];
op = instruction.substring(0, 1);
lop = op.toLowerCase();
// what are the arguments? note that we need to convert
// all strings into numbers, or + will do silly things.
args = instruction
.replace(op, "")
.trim()
.split(" ");
args = args
.filter(function(v) {
return v !== "";
})
.map(parseFloat);
alen = args.length;
// we could use a switch, but elaborate code in a "case" with
// fallthrough is just horrid to read. So let's use ifthen
// statements instead.
// moveto command (plus possible lineto)
if (lop === "m") {
normalized += "M ";
if (op === "m") {
x += args[0];
y += args[1];
} else {
x = args[0];
y = args[1];
}
// records start position, for dealing
// with the shape close operator ('Z')
sx = x;
sy = y;
normalized += x + " " + y + " ";
if (alen > 2) {
for (a = 0; a < alen; a += 2) {
if (op === "m") {
x += args[a];
y += args[a + 1];
} else {
x = args[a];
y = args[a + 1];
}
normalized += ["L",x,y,''].join(" ");
}
}
} else if (lop === "l") {
// lineto commands
for (a = 0; a < alen; a += 2) {
if (op === "l") {
x += args[a];
y += args[a + 1];
} else {
x = args[a];
y = args[a + 1];
}
normalized += ["L",x,y,''].join(" ");
}
} else if (lop === "h") {
for (a = 0; a < alen; a++) {
if (op === "h") {
x += args[a];
} else {
x = args[a];
}
normalized += ["L",x,y,''].join(" ");
}
} else if (lop === "v") {
for (a = 0; a < alen; a++) {
if (op === "v") {
y += args[a];
} else {
y = args[a];
}
normalized += ["L",x,y,''].join(" ");
}
} else if (lop === "q") {
// quadratic curveto commands
for (a = 0; a < alen; a += 4) {
if (op === "q") {
cx = x + args[a];
cy = y + args[a + 1];
x += args[a + 2];
y += args[a + 3];
} else {
cx = args[a];
cy = args[a + 1];
x = args[a + 2];
y = args[a + 3];
}
normalized += ["Q",cx,cy,x,y,''].join(" ");
}
} else if (lop === "t") {
for (a = 0; a < alen; a += 2) {
// reflect previous cx/cy over x/y
cx = x + (x - cx);
cy = y + (y - cy);
// then get real end point
if (op === "t") {
x += args[a];
y += args[a + 1];
} else {
x = args[a];
y = args[a + 1];
}
normalized += ["Q",cx,cy,x,y,''].join(" ");
}
} else if (lop === "c") {
// cubic curveto commands
for (a = 0; a < alen; a += 6) {
if (op === "c") {
cx = x + args[a];
cy = y + args[a + 1];
cx2 = x + args[a + 2];
cy2 = y + args[a + 3];
x += args[a + 4];
y += args[a + 5];
} else {
cx = args[a];
cy = args[a + 1];
cx2 = args[a + 2];
cy2 = args[a + 3];
x = args[a + 4];
y = args[a + 5];
}
normalized += ["C",cx,cy,cx2,cy2,x,y,''].join(" ");
}
} else if (lop === "s") {
for (a = 0; a < alen; a += 4) {
// reflect previous cx2/cy2 over x/y
cx = x + (x - cx2);
cy = y + (y - cy2);
// then get real control and end point
if (op === "s") {
cx2 = x + args[a];
cy2 = y + args[a + 1];
x += args[a + 2];
y += args[a + 3];
} else {
cx2 = args[a];
cy2 = args[a + 1];
x = args[a + 2];
y = args[a + 3];
}
normalized +=["C",cx,cy,cx2,cy2,x,y,''].join(" ");
}
} else if (lop === "z") {
normalized += "Z ";
// not unimportant: path closing changes the current x/y coordinate
x = sx;
y = sy;
}
}
return normalized.trim();
}
module.exports = normalizePath;

View File

@@ -1,68 +0,0 @@
(function() {
"use strict";
var utils = require("./utils.js");
/**
* Poly Bezier
* @param {[type]} curves [description]
*/
var PolyBezier = function(curves) {
this.curves = [];
this._3d = false;
if (!!curves) {
this.curves = curves;
this._3d = this.curves[0]._3d;
}
};
PolyBezier.prototype = {
valueOf: function() {
return this.toString();
},
toString: function() {
return (
"[" +
this.curves
.map(function(curve) {
return utils.pointsToString(curve.points);
})
.join(", ") +
"]"
);
},
addCurve: function(curve) {
this.curves.push(curve);
this._3d = this._3d || curve._3d;
},
length: function() {
return this.curves
.map(function(v) {
return v.length();
})
.reduce(function(a, b) {
return a + b;
});
},
curve: function(idx) {
return this.curves[idx];
},
bbox: function() {
var c = this.curves;
var bbox = c[0].bbox();
for (var i = 1; i < c.length; i++) {
utils.expandbox(bbox, c[i].bbox());
}
return bbox;
},
offset: function(d) {
var offset = [];
this.curves.forEach(function(v) {
offset = offset.concat(v.offset(d));
});
return new PolyBezier(offset);
}
};
module.exports = PolyBezier;
})();

View File

@@ -1,41 +0,0 @@
var normalise = require("./normalise-svg.js");
var M = { x: false, y: false };
function makeBezier(Bezier, term, values) {
if (term === 'Z') return;
if (term === 'M') {
M = {x: values[0], y: values[1]};
return;
}
// ES7: new Bezier(M.x, M.y, ...values)
var cvalues = [false, M.x, M.y].concat(values);
var PreboundConstructor = Bezier.bind.apply(Bezier, cvalues)
var curve = new PreboundConstructor();
var last = values.slice(-2);
M = { x : last[0], y: last[1] };
return curve;
}
function convertPath(Bezier, d) {
var terms = normalise(d).split(" "),
term,
matcher = new RegExp("[MLCQZ]", ""),
segment,
values,
segments = [],
ARGS = { "C": 6, "Q": 4, "L": 2, "M": 2};
while (terms.length) {
term = terms.splice(0,1)[0];
if (matcher.test(term)) {
values = terms.splice(0, ARGS[term]).map(parseFloat);
segment = makeBezier(Bezier, term, values);
if (segment) segments.push(segment);
}
}
return new Bezier.PolyBezier(segments);
}
module.exports = convertPath;

View File

@@ -1,893 +0,0 @@
(function() {
"use strict";
// math-inlining.
var abs = Math.abs,
cos = Math.cos,
sin = Math.sin,
acos = Math.acos,
atan2 = Math.atan2,
sqrt = Math.sqrt,
pow = Math.pow,
// cube root function yielding real roots
crt = function(v) {
return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3);
},
// trig constants
pi = Math.PI,
tau = 2 * pi,
quart = pi / 2,
// float precision significant decimal
epsilon = 0.000001,
// extremas used in bbox calculation and similar algorithms
nMax = Number.MAX_SAFE_INTEGER || 9007199254740991,
nMin = Number.MIN_SAFE_INTEGER || -9007199254740991,
// a zero coordinate, which is surprisingly useful
ZERO = { x: 0, y: 0, z: 0 };
// Bezier utility functions
var utils = {
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
Tvalues: [
-0.0640568928626056260850430826247450385909,
0.0640568928626056260850430826247450385909,
-0.1911188674736163091586398207570696318404,
0.1911188674736163091586398207570696318404,
-0.3150426796961633743867932913198102407864,
0.3150426796961633743867932913198102407864,
-0.4337935076260451384870842319133497124524,
0.4337935076260451384870842319133497124524,
-0.5454214713888395356583756172183723700107,
0.5454214713888395356583756172183723700107,
-0.6480936519369755692524957869107476266696,
0.6480936519369755692524957869107476266696,
-0.7401241915785543642438281030999784255232,
0.7401241915785543642438281030999784255232,
-0.8200019859739029219539498726697452080761,
0.8200019859739029219539498726697452080761,
-0.8864155270044010342131543419821967550873,
0.8864155270044010342131543419821967550873,
-0.9382745520027327585236490017087214496548,
0.9382745520027327585236490017087214496548,
-0.9747285559713094981983919930081690617411,
0.9747285559713094981983919930081690617411,
-0.9951872199970213601799974097007368118745,
0.9951872199970213601799974097007368118745
],
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
Cvalues: [
0.1279381953467521569740561652246953718517,
0.1279381953467521569740561652246953718517,
0.1258374563468282961213753825111836887264,
0.1258374563468282961213753825111836887264,
0.121670472927803391204463153476262425607,
0.121670472927803391204463153476262425607,
0.1155056680537256013533444839067835598622,
0.1155056680537256013533444839067835598622,
0.1074442701159656347825773424466062227946,
0.1074442701159656347825773424466062227946,
0.0976186521041138882698806644642471544279,
0.0976186521041138882698806644642471544279,
0.086190161531953275917185202983742667185,
0.086190161531953275917185202983742667185,
0.0733464814110803057340336152531165181193,
0.0733464814110803057340336152531165181193,
0.0592985849154367807463677585001085845412,
0.0592985849154367807463677585001085845412,
0.0442774388174198061686027482113382288593,
0.0442774388174198061686027482113382288593,
0.0285313886289336631813078159518782864491,
0.0285313886289336631813078159518782864491,
0.0123412297999871995468056670700372915759,
0.0123412297999871995468056670700372915759
],
arcfn: function(t, derivativeFn) {
var d = derivativeFn(t);
var l = d.x * d.x + d.y * d.y;
if (typeof d.z !== "undefined") {
l += d.z * d.z;
}
return sqrt(l);
},
compute: function(t, points, _3d) {
// shortcuts
if (t === 0) {
return points[0];
}
var order = points.length-1;
if (t === 1) {
return points[order];
}
var p = points;
var mt = 1 - t;
// constant?
if (order === 0) {
return points[0];
}
// linear?
if (order === 1) {
ret = {
x: mt * p[0].x + t * p[1].x,
y: mt * p[0].y + t * p[1].y
};
if (_3d) {
ret.z = mt * p[0].z + t * p[1].z;
}
return ret;
}
// quadratic/cubic curve?
if (order < 4) {
var mt2 = mt * mt,
t2 = t * t,
a,
b,
c,
d = 0;
if (order === 2) {
p = [p[0], p[1], p[2], ZERO];
a = mt2;
b = mt * t * 2;
c = t2;
} else if (order === 3) {
a = mt2 * mt;
b = mt2 * t * 3;
c = mt * t2 * 3;
d = t * t2;
}
var ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y
};
if (_3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
}
return ret;
}
// higher order curves: use de Casteljau's computation
var dCpts = JSON.parse(JSON.stringify(points));
while (dCpts.length > 1) {
for (var i = 0; i < dCpts.length - 1; i++) {
dCpts[i] = {
x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t,
y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t
};
if (typeof dCpts[i].z !== "undefined") {
dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t;
}
}
dCpts.splice(dCpts.length - 1, 1);
}
return dCpts[0];
},
computeWithRatios: function (t, points, ratios, _3d) {
var mt = 1 - t, r = ratios, p = points, d;
var f1 = r[0], f2 = r[1], f3 = r[2], f4 = r[3];
// spec for linear
f1 *= mt;
f2 *= t;
if (p.length === 2) {
d = f1 + f2;
return {
x: (f1 * p[0].x + f2 * p[1].x)/d,
y: (f1 * p[0].y + f2 * p[1].y)/d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z)/d
};
}
// upgrade to quadratic
f1 *= mt;
f2 *= 2 * mt;
f3 *= t * t;
if (p.length === 3) {
d = f1 + f2 + f3;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x)/d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y)/d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z)/d
};
}
// upgrade to cubic
f1 *= mt;
f2 *= 1.5 * mt;
f3 *= 3 * mt;
f4 *= t * t * t;
if (p.length === 4) {
d = f1 + f2 + f3 + f4;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x)/d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y)/d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z)/d
};
}
},
derive: function (points, _3d) {
var dpoints = [];
for (var p = points, d = p.length, c = d - 1; d > 1; d--, c--) {
var list = [];
for (var j = 0, dpt; j < c; j++) {
dpt = {
x: c * (p[j + 1].x - p[j].x),
y: c * (p[j + 1].y - p[j].y)
};
if (_3d) {
dpt.z = c * (p[j + 1].z - p[j].z);
}
list.push(dpt);
}
dpoints.push(list);
p = list;
}
return dpoints;
},
between: function(v, m, M) {
return (
(m <= v && v <= M) ||
utils.approximately(v, m) ||
utils.approximately(v, M)
);
},
approximately: function(a, b, precision) {
return abs(a - b) <= (precision || epsilon);
},
length: function(derivativeFn) {
var z = 0.5,
sum = 0,
len = utils.Tvalues.length,
i,
t;
for (i = 0; i < len; i++) {
t = z * utils.Tvalues[i] + z;
sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn);
}
return z * sum;
},
map: function(v, ds, de, ts, te) {
var d1 = de - ds,
d2 = te - ts,
v2 = v - ds,
r = v2 / d1;
return ts + d2 * r;
},
lerp: function(r, v1, v2) {
var ret = {
x: v1.x + r * (v2.x - v1.x),
y: v1.y + r * (v2.y - v1.y)
};
if (!!v1.z && !!v2.z) {
ret.z = v1.z + r * (v2.z - v1.z);
}
return ret;
},
pointToString: function(p) {
var s = p.x + "/" + p.y;
if (typeof p.z !== "undefined") {
s += "/" + p.z;
}
return s;
},
pointsToString: function(points) {
return "[" + points.map(utils.pointToString).join(", ") + "]";
},
copy: function(obj) {
return JSON.parse(JSON.stringify(obj));
},
angle: function(o, v1, v2) {
var dx1 = v1.x - o.x,
dy1 = v1.y - o.y,
dx2 = v2.x - o.x,
dy2 = v2.y - o.y,
cross = dx1 * dy2 - dy1 * dx2,
dot = dx1 * dx2 + dy1 * dy2;
return atan2(cross, dot);
},
// round as string, to avoid rounding errors
round: function(v, d) {
var s = "" + v;
var pos = s.indexOf(".");
return parseFloat(s.substring(0, pos + 1 + d));
},
dist: function(p1, p2) {
var dx = p1.x - p2.x,
dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
},
closest: function(LUT, point) {
var mdist = pow(2, 63),
mpos,
d;
LUT.forEach(function(p, idx) {
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
mpos = idx;
}
});
return { mdist: mdist, mpos: mpos };
},
abcratio: function(t, n) {
// see ratio(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
var bottom = pow(t, n) + pow(1 - t, n),
top = bottom - 1;
return abs(top / bottom);
},
projectionratio: function(t, n) {
// see u(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
var top = pow(1 - t, n),
bottom = pow(t, n) + top;
return top / bottom;
},
lli8: function(x1, y1, x2, y2, x3, y3, x4, y4) {
var nx =
(x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (d == 0) {
return false;
}
return { x: nx / d, y: ny / d };
},
lli4: function(p1, p2, p3, p4) {
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
x3 = p3.x,
y3 = p3.y,
x4 = p4.x,
y4 = p4.y;
return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4);
},
lli: function(v1, v2) {
return utils.lli4(v1, v1.c, v2, v2.c);
},
makeline: function(p1, p2) {
var Bezier = require("./bezier");
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
dx = (x2 - x1) / 3,
dy = (y2 - y1) / 3;
return new Bezier(
x1,
y1,
x1 + dx,
y1 + dy,
x1 + 2 * dx,
y1 + 2 * dy,
x2,
y2
);
},
findbbox: function(sections) {
var mx = nMax,
my = nMax,
MX = nMin,
MY = nMin;
sections.forEach(function(s) {
var bbox = s.bbox();
if (mx > bbox.x.min) mx = bbox.x.min;
if (my > bbox.y.min) my = bbox.y.min;
if (MX < bbox.x.max) MX = bbox.x.max;
if (MY < bbox.y.max) MY = bbox.y.max;
});
return {
x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx },
y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my }
};
},
shapeintersections: function(
s1,
bbox1,
s2,
bbox2,
curveIntersectionThreshold
) {
if (!utils.bboxoverlap(bbox1, bbox2)) return [];
var intersections = [];
var a1 = [s1.startcap, s1.forward, s1.back, s1.endcap];
var a2 = [s2.startcap, s2.forward, s2.back, s2.endcap];
a1.forEach(function(l1) {
if (l1.virtual) return;
a2.forEach(function(l2) {
if (l2.virtual) return;
var iss = l1.intersects(l2, curveIntersectionThreshold);
if (iss.length > 0) {
iss.c1 = l1;
iss.c2 = l2;
iss.s1 = s1;
iss.s2 = s2;
intersections.push(iss);
}
});
});
return intersections;
},
makeshape: function(forward, back, curveIntersectionThreshold) {
var bpl = back.points.length;
var fpl = forward.points.length;
var start = utils.makeline(back.points[bpl - 1], forward.points[0]);
var end = utils.makeline(forward.points[fpl - 1], back.points[0]);
var shape = {
startcap: start,
forward: forward,
back: back,
endcap: end,
bbox: utils.findbbox([start, forward, back, end])
};
var self = utils;
shape.intersections = function(s2) {
return self.shapeintersections(
shape,
shape.bbox,
s2,
s2.bbox,
curveIntersectionThreshold
);
};
return shape;
},
getminmax: function(curve, d, list) {
if (!list) return { min: 0, max: 0 };
var min = nMax,
max = nMin,
t,
c;
if (list.indexOf(0) === -1) {
list = [0].concat(list);
}
if (list.indexOf(1) === -1) {
list.push(1);
}
for (var i = 0, len = list.length; i < len; i++) {
t = list[i];
c = curve.get(t);
if (c[d] < min) {
min = c[d];
}
if (c[d] > max) {
max = c[d];
}
}
return { min: min, mid: (min + max) / 2, max: max, size: max - min };
},
align: function(points, line) {
var tx = line.p1.x,
ty = line.p1.y,
a = -atan2(line.p2.y - ty, line.p2.x - tx),
d = function(v) {
return {
x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a),
y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a)
};
};
return points.map(d);
},
roots: function(points, line) {
line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } };
var order = points.length - 1;
var p = utils.align(points, line);
var reduce = function(t) {
return 0 <= t && t <= 1;
};
if (order === 2) {
var a = p[0].y,
b = p[1].y,
c = p[2].y,
d = a - 2 * b + c;
if (d !== 0) {
var m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2].filter(reduce);
} else if (b !== c && d === 0) {
return [(2*b - c)/(2*b - 2*c)].filter(reduce);
}
return [];
}
// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
var pa = p[0].y,
pb = p[1].y,
pc = p[2].y,
pd = p[3].y,
d = -pa + 3 * pb - 3 * pc + pd,
a = 3 * pa - 6 * pb + 3 * pc,
b = -3 * pa + 3 * pb,
c = pa;
if (utils.approximately(d, 0)) {
// this is not a cubic curve.
if (utils.approximately(a, 0)) {
// in fact, this is not a quadratic curve either.
if (utils.approximately(b, 0)) {
// in fact in fact, there are no solutions.
return [];
}
// linear solution:
return [-c / b].filter(reduce);
}
// quadratic solution:
var q = sqrt(b * b - 4 * a * c),
a2 = 2 * a;
return [(q - b) / a2, (-b - q) / a2].filter(reduce);
}
// at this point, we know we need a cubic solution:
a /= d;
b /= d;
c /= d;
var p = (3 * b - a * a) / 3,
p3 = p / 3,
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27,
q2 = q / 2,
discriminant = q2 * q2 + p3 * p3 * p3,
u1,
v1,
x1,
x2,
x3;
if (discriminant < 0) {
var mp3 = -p / 3,
mp33 = mp3 * mp3 * mp3,
r = sqrt(mp33),
t = -q / (2 * r),
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
phi = acos(cosphi),
crtr = crt(r),
t1 = 2 * crtr;
x1 = t1 * cos(phi / 3) - a / 3;
x2 = t1 * cos((phi + tau) / 3) - a / 3;
x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3;
return [x1, x2, x3].filter(reduce);
} else if (discriminant === 0) {
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
x1 = 2 * u1 - a / 3;
x2 = -u1 - a / 3;
return [x1, x2].filter(reduce);
} else {
var sd = sqrt(discriminant);
u1 = crt(-q2 + sd);
v1 = crt(q2 + sd);
return [u1 - v1 - a / 3].filter(reduce);
}
},
droots: function(p) {
// quadratic roots are easy
if (p.length === 3) {
var a = p[0],
b = p[1],
c = p[2],
d = a - 2 * b + c;
if (d !== 0) {
var m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2];
} else if (b !== c && d === 0) {
return [(2 * b - c) / (2 * (b - c))];
}
return [];
}
// linear roots are even easier
if (p.length === 2) {
var a = p[0],
b = p[1];
if (a !== b) {
return [a / (a - b)];
}
return [];
}
},
curvature: function(t, points, _3d, kOnly) {
var dpoints = utils.derive(points);
var d1 = dpoints[0];
var d2 = dpoints[1];
var num, dnm, adk, dk, k=0, r=0;
//
// We're using the following formula for curvature:
//
// x'y" - y'x"
// k(t) = ------------------
// (x'² + y'²)^(3/2)
//
// from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition
//
// With it corresponding 3D counterpart:
//
// sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²)
// k(t) = -------------------------------------------------------
// (x'² + y'² + z'²)^(3/2)
//
var d = utils.compute(t, d1);
var dd = utils.compute(t, d2);
var qdsum = d.x*d.x + d.y*d.y;
if (_3d) {
num = sqrt(
pow(d.y*dd.z - dd.y*d.z, 2) +
pow(d.z*dd.x - dd.z*d.x, 2) +
pow(d.x*dd.y - dd.x*d.y, 2)
);
dnm = pow(qdsum + d.z*d.z, 3/2);
} else {
num = d.x*dd.y - d.y*dd.x;
dnm = pow(qdsum, 3/2);
}
if (num === 0 || dnm === 0) {
return { k:0, r:0 };
}
k = num/dnm;
r = dnm/num;
// We're also computing the derivative of kappa, because
// there is value in knowing the rate of change for the
// curvature along the curve. And we're just going to
// ballpark it based on an epsilon.
if (!kOnly) {
// compute k'(t) based on the interval before, and after it,
// to at least try to not introduce forward/backward pass bias.
var pk = utils.curvature(t-0.001, points, _3d, true).k;
var nk = utils.curvature(t+0.001, points, _3d, true).k;
dk = ((nk-k) + (k-pk))/2;
adk = (abs(nk-k) + abs(k-pk))/2;
}
return { k: k, r: r, dk: dk, adk:adk, };
},
inflections: function(points) {
if (points.length < 4) return [];
// FIXME: TODO: add in inflection abstraction for quartic+ curves?
var p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }),
a = p[2].x * p[1].y,
b = p[3].x * p[1].y,
c = p[1].x * p[2].y,
d = p[3].x * p[2].y,
v1 = 18 * (-3 * a + 2 * b + 3 * c - d),
v2 = 18 * (3 * a - b - 3 * c),
v3 = 18 * (c - a);
if (utils.approximately(v1, 0)) {
if (!utils.approximately(v2, 0)) {
var t = -v3 / v2;
if (0 <= t && t <= 1) return [t];
}
return [];
}
var trm = v2 * v2 - 4 * v1 * v3,
sq = Math.sqrt(trm),
d = 2 * v1;
if (utils.approximately(d, 0)) return [];
return [(sq - v2) / d, -(v2 + sq) / d].filter(function(r) {
return 0 <= r && r <= 1;
});
},
bboxoverlap: function(b1, b2) {
var dims = ["x", "y"],
len = dims.length,
i,
dim,
l,
t,
d;
for (i = 0; i < len; i++) {
dim = dims[i];
l = b1[dim].mid;
t = b2[dim].mid;
d = (b1[dim].size + b2[dim].size) / 2;
if (abs(l - t) >= d) return false;
}
return true;
},
expandbox: function(bbox, _bbox) {
if (_bbox.x.min < bbox.x.min) {
bbox.x.min = _bbox.x.min;
}
if (_bbox.y.min < bbox.y.min) {
bbox.y.min = _bbox.y.min;
}
if (_bbox.z && _bbox.z.min < bbox.z.min) {
bbox.z.min = _bbox.z.min;
}
if (_bbox.x.max > bbox.x.max) {
bbox.x.max = _bbox.x.max;
}
if (_bbox.y.max > bbox.y.max) {
bbox.y.max = _bbox.y.max;
}
if (_bbox.z && _bbox.z.max > bbox.z.max) {
bbox.z.max = _bbox.z.max;
}
bbox.x.mid = (bbox.x.min + bbox.x.max) / 2;
bbox.y.mid = (bbox.y.min + bbox.y.max) / 2;
if (bbox.z) {
bbox.z.mid = (bbox.z.min + bbox.z.max) / 2;
}
bbox.x.size = bbox.x.max - bbox.x.min;
bbox.y.size = bbox.y.max - bbox.y.min;
if (bbox.z) {
bbox.z.size = bbox.z.max - bbox.z.min;
}
},
pairiteration: function(c1, c2, curveIntersectionThreshold) {
var c1b = c1.bbox(),
c2b = c2.bbox(),
r = 100000,
threshold = curveIntersectionThreshold || 0.5;
if (
c1b.x.size + c1b.y.size < threshold &&
c2b.x.size + c2b.y.size < threshold
) {
return [
((r * (c1._t1 + c1._t2) / 2) | 0) / r +
"/" +
((r * (c2._t1 + c2._t2) / 2) | 0) / r
];
}
var cc1 = c1.split(0.5),
cc2 = c2.split(0.5),
pairs = [
{ left: cc1.left, right: cc2.left },
{ left: cc1.left, right: cc2.right },
{ left: cc1.right, right: cc2.right },
{ left: cc1.right, right: cc2.left }
];
pairs = pairs.filter(function(pair) {
return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox());
});
var results = [];
if (pairs.length === 0) return results;
pairs.forEach(function(pair) {
results = results.concat(
utils.pairiteration(pair.left, pair.right, threshold)
);
});
results = results.filter(function(v, i) {
return results.indexOf(v) === i;
});
return results;
},
getccenter: function(p1, p2, p3) {
var dx1 = p2.x - p1.x,
dy1 = p2.y - p1.y,
dx2 = p3.x - p2.x,
dy2 = p3.y - p2.y;
var dx1p = dx1 * cos(quart) - dy1 * sin(quart),
dy1p = dx1 * sin(quart) + dy1 * cos(quart),
dx2p = dx2 * cos(quart) - dy2 * sin(quart),
dy2p = dx2 * sin(quart) + dy2 * cos(quart);
// chord midpoints
var mx1 = (p1.x + p2.x) / 2,
my1 = (p1.y + p2.y) / 2,
mx2 = (p2.x + p3.x) / 2,
my2 = (p2.y + p3.y) / 2;
// midpoint offsets
var mx1n = mx1 + dx1p,
my1n = my1 + dy1p,
mx2n = mx2 + dx2p,
my2n = my2 + dy2p;
// intersection of these lines:
var arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n),
r = utils.dist(arc, p1),
// arc start/end values, over mid point:
s = atan2(p1.y - arc.y, p1.x - arc.x),
m = atan2(p2.y - arc.y, p2.x - arc.x),
e = atan2(p3.y - arc.y, p3.x - arc.x),
_;
// determine arc direction (cw/ccw correction)
if (s < e) {
// if s<m<e, arc(s, e)
// if m<s<e, arc(e, s + tau)
// if s<e<m, arc(e, s + tau)
if (s > m || m > e) {
s += tau;
}
if (s > e) {
_ = e;
e = s;
s = _;
}
} else {
// if e<m<s, arc(e, s)
// if m<e<s, arc(s, e + tau)
// if e<s<m, arc(s, e + tau)
if (e < m && m < s) {
_ = e;
e = s;
s = _;
} else {
e += tau;
}
}
// assign and done.
arc.s = s;
arc.e = e;
arc.r = r;
return arc;
},
numberSort: function(a, b) {
return a - b;
}
};
module.exports = utils;
})();

View File

@@ -1,68 +0,0 @@
(function() {
"use strict";
var utils = require("./utils.js");
/**
* Poly Bezier
* @param {[type]} curves [description]
*/
var PolyBezier = function(curves) {
this.curves = [];
this._3d = false;
if (!!curves) {
this.curves = curves;
this._3d = this.curves[0]._3d;
}
};
PolyBezier.prototype = {
valueOf: function() {
return this.toString();
},
toString: function() {
return (
"[" +
this.curves
.map(function(curve) {
return utils.pointsToString(curve.points);
})
.join(", ") +
"]"
);
},
addCurve: function(curve) {
this.curves.push(curve);
this._3d = this._3d || curve._3d;
},
length: function() {
return this.curves
.map(function(v) {
return v.length();
})
.reduce(function(a, b) {
return a + b;
});
},
curve: function(idx) {
return this.curves[idx];
},
bbox: function() {
var c = this.curves;
var bbox = c[0].bbox();
for (var i = 1; i < c.length; i++) {
utils.expandbox(bbox, c[i].bbox());
}
return bbox;
},
offset: function(d) {
var offset = [];
this.curves.forEach(function(v) {
offset = offset.concat(v.offset(d));
});
return new PolyBezier(offset);
}
};
module.exports = PolyBezier;
})();

View File

@@ -1,41 +0,0 @@
var normalise = require("./normalise-svg.js");
var M = { x: false, y: false };
function makeBezier(Bezier, term, values) {
if (term === 'Z') return;
if (term === 'M') {
M = {x: values[0], y: values[1]};
return;
}
// ES7: new Bezier(M.x, M.y, ...values)
var cvalues = [false, M.x, M.y].concat(values);
var PreboundConstructor = Bezier.bind.apply(Bezier, cvalues)
var curve = new PreboundConstructor();
var last = values.slice(-2);
M = { x : last[0], y: last[1] };
return curve;
}
function convertPath(Bezier, d) {
var terms = normalise(d).split(" "),
term,
matcher = new RegExp("[MLCQZ]", ""),
segment,
values,
segments = [],
ARGS = { "C": 6, "Q": 4, "L": 2, "M": 2};
while (terms.length) {
term = terms.splice(0,1)[0];
if (matcher.test(term)) {
values = terms.splice(0, ARGS[term]).map(parseFloat);
segment = makeBezier(Bezier, term, values);
if (segment) segments.push(segment);
}
}
return new Bezier.PolyBezier(segments);
}
module.exports = convertPath;

View File

@@ -1,893 +0,0 @@
(function() {
"use strict";
// math-inlining.
var abs = Math.abs,
cos = Math.cos,
sin = Math.sin,
acos = Math.acos,
atan2 = Math.atan2,
sqrt = Math.sqrt,
pow = Math.pow,
// cube root function yielding real roots
crt = function(v) {
return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3);
},
// trig constants
pi = Math.PI,
tau = 2 * pi,
quart = pi / 2,
// float precision significant decimal
epsilon = 0.000001,
// extremas used in bbox calculation and similar algorithms
nMax = Number.MAX_SAFE_INTEGER || 9007199254740991,
nMin = Number.MIN_SAFE_INTEGER || -9007199254740991,
// a zero coordinate, which is surprisingly useful
ZERO = { x: 0, y: 0, z: 0 };
// Bezier utility functions
var utils = {
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
Tvalues: [
-0.0640568928626056260850430826247450385909,
0.0640568928626056260850430826247450385909,
-0.1911188674736163091586398207570696318404,
0.1911188674736163091586398207570696318404,
-0.3150426796961633743867932913198102407864,
0.3150426796961633743867932913198102407864,
-0.4337935076260451384870842319133497124524,
0.4337935076260451384870842319133497124524,
-0.5454214713888395356583756172183723700107,
0.5454214713888395356583756172183723700107,
-0.6480936519369755692524957869107476266696,
0.6480936519369755692524957869107476266696,
-0.7401241915785543642438281030999784255232,
0.7401241915785543642438281030999784255232,
-0.8200019859739029219539498726697452080761,
0.8200019859739029219539498726697452080761,
-0.8864155270044010342131543419821967550873,
0.8864155270044010342131543419821967550873,
-0.9382745520027327585236490017087214496548,
0.9382745520027327585236490017087214496548,
-0.9747285559713094981983919930081690617411,
0.9747285559713094981983919930081690617411,
-0.9951872199970213601799974097007368118745,
0.9951872199970213601799974097007368118745
],
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
Cvalues: [
0.1279381953467521569740561652246953718517,
0.1279381953467521569740561652246953718517,
0.1258374563468282961213753825111836887264,
0.1258374563468282961213753825111836887264,
0.121670472927803391204463153476262425607,
0.121670472927803391204463153476262425607,
0.1155056680537256013533444839067835598622,
0.1155056680537256013533444839067835598622,
0.1074442701159656347825773424466062227946,
0.1074442701159656347825773424466062227946,
0.0976186521041138882698806644642471544279,
0.0976186521041138882698806644642471544279,
0.086190161531953275917185202983742667185,
0.086190161531953275917185202983742667185,
0.0733464814110803057340336152531165181193,
0.0733464814110803057340336152531165181193,
0.0592985849154367807463677585001085845412,
0.0592985849154367807463677585001085845412,
0.0442774388174198061686027482113382288593,
0.0442774388174198061686027482113382288593,
0.0285313886289336631813078159518782864491,
0.0285313886289336631813078159518782864491,
0.0123412297999871995468056670700372915759,
0.0123412297999871995468056670700372915759
],
arcfn: function(t, derivativeFn) {
var d = derivativeFn(t);
var l = d.x * d.x + d.y * d.y;
if (typeof d.z !== "undefined") {
l += d.z * d.z;
}
return sqrt(l);
},
compute: function(t, points, _3d) {
// shortcuts
if (t === 0) {
return points[0];
}
var order = points.length-1;
if (t === 1) {
return points[order];
}
var p = points;
var mt = 1 - t;
// constant?
if (order === 0) {
return points[0];
}
// linear?
if (order === 1) {
ret = {
x: mt * p[0].x + t * p[1].x,
y: mt * p[0].y + t * p[1].y
};
if (_3d) {
ret.z = mt * p[0].z + t * p[1].z;
}
return ret;
}
// quadratic/cubic curve?
if (order < 4) {
var mt2 = mt * mt,
t2 = t * t,
a,
b,
c,
d = 0;
if (order === 2) {
p = [p[0], p[1], p[2], ZERO];
a = mt2;
b = mt * t * 2;
c = t2;
} else if (order === 3) {
a = mt2 * mt;
b = mt2 * t * 3;
c = mt * t2 * 3;
d = t * t2;
}
var ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y
};
if (_3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
}
return ret;
}
// higher order curves: use de Casteljau's computation
var dCpts = JSON.parse(JSON.stringify(points));
while (dCpts.length > 1) {
for (var i = 0; i < dCpts.length - 1; i++) {
dCpts[i] = {
x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t,
y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t
};
if (typeof dCpts[i].z !== "undefined") {
dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t;
}
}
dCpts.splice(dCpts.length - 1, 1);
}
return dCpts[0];
},
computeWithRatios: function (t, points, ratios, _3d) {
var mt = 1 - t, r = ratios, p = points, d;
var f1 = r[0], f2 = r[1], f3 = r[2], f4 = r[3];
// spec for linear
f1 *= mt;
f2 *= t;
if (p.length === 2) {
d = f1 + f2;
return {
x: (f1 * p[0].x + f2 * p[1].x)/d,
y: (f1 * p[0].y + f2 * p[1].y)/d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z)/d
};
}
// upgrade to quadratic
f1 *= mt;
f2 *= 2 * mt;
f3 *= t * t;
if (p.length === 3) {
d = f1 + f2 + f3;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x)/d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y)/d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z)/d
};
}
// upgrade to cubic
f1 *= mt;
f2 *= 1.5 * mt;
f3 *= 3 * mt;
f4 *= t * t * t;
if (p.length === 4) {
d = f1 + f2 + f3 + f4;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x)/d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y)/d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z)/d
};
}
},
derive: function (points, _3d) {
var dpoints = [];
for (var p = points, d = p.length, c = d - 1; d > 1; d--, c--) {
var list = [];
for (var j = 0, dpt; j < c; j++) {
dpt = {
x: c * (p[j + 1].x - p[j].x),
y: c * (p[j + 1].y - p[j].y)
};
if (_3d) {
dpt.z = c * (p[j + 1].z - p[j].z);
}
list.push(dpt);
}
dpoints.push(list);
p = list;
}
return dpoints;
},
between: function(v, m, M) {
return (
(m <= v && v <= M) ||
utils.approximately(v, m) ||
utils.approximately(v, M)
);
},
approximately: function(a, b, precision) {
return abs(a - b) <= (precision || epsilon);
},
length: function(derivativeFn) {
var z = 0.5,
sum = 0,
len = utils.Tvalues.length,
i,
t;
for (i = 0; i < len; i++) {
t = z * utils.Tvalues[i] + z;
sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn);
}
return z * sum;
},
map: function(v, ds, de, ts, te) {
var d1 = de - ds,
d2 = te - ts,
v2 = v - ds,
r = v2 / d1;
return ts + d2 * r;
},
lerp: function(r, v1, v2) {
var ret = {
x: v1.x + r * (v2.x - v1.x),
y: v1.y + r * (v2.y - v1.y)
};
if (!!v1.z && !!v2.z) {
ret.z = v1.z + r * (v2.z - v1.z);
}
return ret;
},
pointToString: function(p) {
var s = p.x + "/" + p.y;
if (typeof p.z !== "undefined") {
s += "/" + p.z;
}
return s;
},
pointsToString: function(points) {
return "[" + points.map(utils.pointToString).join(", ") + "]";
},
copy: function(obj) {
return JSON.parse(JSON.stringify(obj));
},
angle: function(o, v1, v2) {
var dx1 = v1.x - o.x,
dy1 = v1.y - o.y,
dx2 = v2.x - o.x,
dy2 = v2.y - o.y,
cross = dx1 * dy2 - dy1 * dx2,
dot = dx1 * dx2 + dy1 * dy2;
return atan2(cross, dot);
},
// round as string, to avoid rounding errors
round: function(v, d) {
var s = "" + v;
var pos = s.indexOf(".");
return parseFloat(s.substring(0, pos + 1 + d));
},
dist: function(p1, p2) {
var dx = p1.x - p2.x,
dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
},
closest: function(LUT, point) {
var mdist = pow(2, 63),
mpos,
d;
LUT.forEach(function(p, idx) {
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
mpos = idx;
}
});
return { mdist: mdist, mpos: mpos };
},
abcratio: function(t, n) {
// see ratio(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
var bottom = pow(t, n) + pow(1 - t, n),
top = bottom - 1;
return abs(top / bottom);
},
projectionratio: function(t, n) {
// see u(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
var top = pow(1 - t, n),
bottom = pow(t, n) + top;
return top / bottom;
},
lli8: function(x1, y1, x2, y2, x3, y3, x4, y4) {
var nx =
(x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (d == 0) {
return false;
}
return { x: nx / d, y: ny / d };
},
lli4: function(p1, p2, p3, p4) {
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
x3 = p3.x,
y3 = p3.y,
x4 = p4.x,
y4 = p4.y;
return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4);
},
lli: function(v1, v2) {
return utils.lli4(v1, v1.c, v2, v2.c);
},
makeline: function(p1, p2) {
var Bezier = require("./bezier");
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
dx = (x2 - x1) / 3,
dy = (y2 - y1) / 3;
return new Bezier(
x1,
y1,
x1 + dx,
y1 + dy,
x1 + 2 * dx,
y1 + 2 * dy,
x2,
y2
);
},
findbbox: function(sections) {
var mx = nMax,
my = nMax,
MX = nMin,
MY = nMin;
sections.forEach(function(s) {
var bbox = s.bbox();
if (mx > bbox.x.min) mx = bbox.x.min;
if (my > bbox.y.min) my = bbox.y.min;
if (MX < bbox.x.max) MX = bbox.x.max;
if (MY < bbox.y.max) MY = bbox.y.max;
});
return {
x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx },
y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my }
};
},
shapeintersections: function(
s1,
bbox1,
s2,
bbox2,
curveIntersectionThreshold
) {
if (!utils.bboxoverlap(bbox1, bbox2)) return [];
var intersections = [];
var a1 = [s1.startcap, s1.forward, s1.back, s1.endcap];
var a2 = [s2.startcap, s2.forward, s2.back, s2.endcap];
a1.forEach(function(l1) {
if (l1.virtual) return;
a2.forEach(function(l2) {
if (l2.virtual) return;
var iss = l1.intersects(l2, curveIntersectionThreshold);
if (iss.length > 0) {
iss.c1 = l1;
iss.c2 = l2;
iss.s1 = s1;
iss.s2 = s2;
intersections.push(iss);
}
});
});
return intersections;
},
makeshape: function(forward, back, curveIntersectionThreshold) {
var bpl = back.points.length;
var fpl = forward.points.length;
var start = utils.makeline(back.points[bpl - 1], forward.points[0]);
var end = utils.makeline(forward.points[fpl - 1], back.points[0]);
var shape = {
startcap: start,
forward: forward,
back: back,
endcap: end,
bbox: utils.findbbox([start, forward, back, end])
};
var self = utils;
shape.intersections = function(s2) {
return self.shapeintersections(
shape,
shape.bbox,
s2,
s2.bbox,
curveIntersectionThreshold
);
};
return shape;
},
getminmax: function(curve, d, list) {
if (!list) return { min: 0, max: 0 };
var min = nMax,
max = nMin,
t,
c;
if (list.indexOf(0) === -1) {
list = [0].concat(list);
}
if (list.indexOf(1) === -1) {
list.push(1);
}
for (var i = 0, len = list.length; i < len; i++) {
t = list[i];
c = curve.get(t);
if (c[d] < min) {
min = c[d];
}
if (c[d] > max) {
max = c[d];
}
}
return { min: min, mid: (min + max) / 2, max: max, size: max - min };
},
align: function(points, line) {
var tx = line.p1.x,
ty = line.p1.y,
a = -atan2(line.p2.y - ty, line.p2.x - tx),
d = function(v) {
return {
x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a),
y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a)
};
};
return points.map(d);
},
roots: function(points, line) {
line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } };
var order = points.length - 1;
var p = utils.align(points, line);
var reduce = function(t) {
return 0 <= t && t <= 1;
};
if (order === 2) {
var a = p[0].y,
b = p[1].y,
c = p[2].y,
d = a - 2 * b + c;
if (d !== 0) {
var m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2].filter(reduce);
} else if (b !== c && d === 0) {
return [(2*b - c)/(2*b - 2*c)].filter(reduce);
}
return [];
}
// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
var pa = p[0].y,
pb = p[1].y,
pc = p[2].y,
pd = p[3].y,
d = -pa + 3 * pb - 3 * pc + pd,
a = 3 * pa - 6 * pb + 3 * pc,
b = -3 * pa + 3 * pb,
c = pa;
if (utils.approximately(d, 0)) {
// this is not a cubic curve.
if (utils.approximately(a, 0)) {
// in fact, this is not a quadratic curve either.
if (utils.approximately(b, 0)) {
// in fact in fact, there are no solutions.
return [];
}
// linear solution:
return [-c / b].filter(reduce);
}
// quadratic solution:
var q = sqrt(b * b - 4 * a * c),
a2 = 2 * a;
return [(q - b) / a2, (-b - q) / a2].filter(reduce);
}
// at this point, we know we need a cubic solution:
a /= d;
b /= d;
c /= d;
var p = (3 * b - a * a) / 3,
p3 = p / 3,
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27,
q2 = q / 2,
discriminant = q2 * q2 + p3 * p3 * p3,
u1,
v1,
x1,
x2,
x3;
if (discriminant < 0) {
var mp3 = -p / 3,
mp33 = mp3 * mp3 * mp3,
r = sqrt(mp33),
t = -q / (2 * r),
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
phi = acos(cosphi),
crtr = crt(r),
t1 = 2 * crtr;
x1 = t1 * cos(phi / 3) - a / 3;
x2 = t1 * cos((phi + tau) / 3) - a / 3;
x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3;
return [x1, x2, x3].filter(reduce);
} else if (discriminant === 0) {
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
x1 = 2 * u1 - a / 3;
x2 = -u1 - a / 3;
return [x1, x2].filter(reduce);
} else {
var sd = sqrt(discriminant);
u1 = crt(-q2 + sd);
v1 = crt(q2 + sd);
return [u1 - v1 - a / 3].filter(reduce);
}
},
droots: function(p) {
// quadratic roots are easy
if (p.length === 3) {
var a = p[0],
b = p[1],
c = p[2],
d = a - 2 * b + c;
if (d !== 0) {
var m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2];
} else if (b !== c && d === 0) {
return [(2 * b - c) / (2 * (b - c))];
}
return [];
}
// linear roots are even easier
if (p.length === 2) {
var a = p[0],
b = p[1];
if (a !== b) {
return [a / (a - b)];
}
return [];
}
},
curvature: function(t, points, _3d, kOnly) {
var dpoints = utils.derive(points);
var d1 = dpoints[0];
var d2 = dpoints[1];
var num, dnm, adk, dk, k=0, r=0;
//
// We're using the following formula for curvature:
//
// x'y" - y'x"
// k(t) = ------------------
// (x'² + y'²)^(3/2)
//
// from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition
//
// With it corresponding 3D counterpart:
//
// sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²)
// k(t) = -------------------------------------------------------
// (x'² + y'² + z'²)^(3/2)
//
var d = utils.compute(t, d1);
var dd = utils.compute(t, d2);
var qdsum = d.x*d.x + d.y*d.y;
if (_3d) {
num = sqrt(
pow(d.y*dd.z - dd.y*d.z, 2) +
pow(d.z*dd.x - dd.z*d.x, 2) +
pow(d.x*dd.y - dd.x*d.y, 2)
);
dnm = pow(qdsum + d.z*d.z, 3/2);
} else {
num = d.x*dd.y - d.y*dd.x;
dnm = pow(qdsum, 3/2);
}
if (num === 0 || dnm === 0) {
return { k:0, r:0 };
}
k = num/dnm;
r = dnm/num;
// We're also computing the derivative of kappa, because
// there is value in knowing the rate of change for the
// curvature along the curve. And we're just going to
// ballpark it based on an epsilon.
if (!kOnly) {
// compute k'(t) based on the interval before, and after it,
// to at least try to not introduce forward/backward pass bias.
var pk = utils.curvature(t-0.001, points, _3d, true).k;
var nk = utils.curvature(t+0.001, points, _3d, true).k;
dk = ((nk-k) + (k-pk))/2;
adk = (abs(nk-k) + abs(k-pk))/2;
}
return { k: k, r: r, dk: dk, adk:adk, };
},
inflections: function(points) {
if (points.length < 4) return [];
// FIXME: TODO: add in inflection abstraction for quartic+ curves?
var p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }),
a = p[2].x * p[1].y,
b = p[3].x * p[1].y,
c = p[1].x * p[2].y,
d = p[3].x * p[2].y,
v1 = 18 * (-3 * a + 2 * b + 3 * c - d),
v2 = 18 * (3 * a - b - 3 * c),
v3 = 18 * (c - a);
if (utils.approximately(v1, 0)) {
if (!utils.approximately(v2, 0)) {
var t = -v3 / v2;
if (0 <= t && t <= 1) return [t];
}
return [];
}
var trm = v2 * v2 - 4 * v1 * v3,
sq = Math.sqrt(trm),
d = 2 * v1;
if (utils.approximately(d, 0)) return [];
return [(sq - v2) / d, -(v2 + sq) / d].filter(function(r) {
return 0 <= r && r <= 1;
});
},
bboxoverlap: function(b1, b2) {
var dims = ["x", "y"],
len = dims.length,
i,
dim,
l,
t,
d;
for (i = 0; i < len; i++) {
dim = dims[i];
l = b1[dim].mid;
t = b2[dim].mid;
d = (b1[dim].size + b2[dim].size) / 2;
if (abs(l - t) >= d) return false;
}
return true;
},
expandbox: function(bbox, _bbox) {
if (_bbox.x.min < bbox.x.min) {
bbox.x.min = _bbox.x.min;
}
if (_bbox.y.min < bbox.y.min) {
bbox.y.min = _bbox.y.min;
}
if (_bbox.z && _bbox.z.min < bbox.z.min) {
bbox.z.min = _bbox.z.min;
}
if (_bbox.x.max > bbox.x.max) {
bbox.x.max = _bbox.x.max;
}
if (_bbox.y.max > bbox.y.max) {
bbox.y.max = _bbox.y.max;
}
if (_bbox.z && _bbox.z.max > bbox.z.max) {
bbox.z.max = _bbox.z.max;
}
bbox.x.mid = (bbox.x.min + bbox.x.max) / 2;
bbox.y.mid = (bbox.y.min + bbox.y.max) / 2;
if (bbox.z) {
bbox.z.mid = (bbox.z.min + bbox.z.max) / 2;
}
bbox.x.size = bbox.x.max - bbox.x.min;
bbox.y.size = bbox.y.max - bbox.y.min;
if (bbox.z) {
bbox.z.size = bbox.z.max - bbox.z.min;
}
},
pairiteration: function(c1, c2, curveIntersectionThreshold) {
var c1b = c1.bbox(),
c2b = c2.bbox(),
r = 100000,
threshold = curveIntersectionThreshold || 0.5;
if (
c1b.x.size + c1b.y.size < threshold &&
c2b.x.size + c2b.y.size < threshold
) {
return [
((r * (c1._t1 + c1._t2) / 2) | 0) / r +
"/" +
((r * (c2._t1 + c2._t2) / 2) | 0) / r
];
}
var cc1 = c1.split(0.5),
cc2 = c2.split(0.5),
pairs = [
{ left: cc1.left, right: cc2.left },
{ left: cc1.left, right: cc2.right },
{ left: cc1.right, right: cc2.right },
{ left: cc1.right, right: cc2.left }
];
pairs = pairs.filter(function(pair) {
return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox());
});
var results = [];
if (pairs.length === 0) return results;
pairs.forEach(function(pair) {
results = results.concat(
utils.pairiteration(pair.left, pair.right, threshold)
);
});
results = results.filter(function(v, i) {
return results.indexOf(v) === i;
});
return results;
},
getccenter: function(p1, p2, p3) {
var dx1 = p2.x - p1.x,
dy1 = p2.y - p1.y,
dx2 = p3.x - p2.x,
dy2 = p3.y - p2.y;
var dx1p = dx1 * cos(quart) - dy1 * sin(quart),
dy1p = dx1 * sin(quart) + dy1 * cos(quart),
dx2p = dx2 * cos(quart) - dy2 * sin(quart),
dy2p = dx2 * sin(quart) + dy2 * cos(quart);
// chord midpoints
var mx1 = (p1.x + p2.x) / 2,
my1 = (p1.y + p2.y) / 2,
mx2 = (p2.x + p3.x) / 2,
my2 = (p2.y + p3.y) / 2;
// midpoint offsets
var mx1n = mx1 + dx1p,
my1n = my1 + dy1p,
mx2n = mx2 + dx2p,
my2n = my2 + dy2p;
// intersection of these lines:
var arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n),
r = utils.dist(arc, p1),
// arc start/end values, over mid point:
s = atan2(p1.y - arc.y, p1.x - arc.x),
m = atan2(p2.y - arc.y, p2.x - arc.x),
e = atan2(p3.y - arc.y, p3.x - arc.x),
_;
// determine arc direction (cw/ccw correction)
if (s < e) {
// if s<m<e, arc(s, e)
// if m<s<e, arc(e, s + tau)
// if s<e<m, arc(e, s + tau)
if (s > m || m > e) {
s += tau;
}
if (s > e) {
_ = e;
e = s;
s = _;
}
} else {
// if e<m<s, arc(e, s)
// if m<e<s, arc(s, e + tau)
// if e<s<m, arc(s, e + tau)
if (e < m && m < s) {
_ = e;
e = s;
s = _;
} else {
e += tau;
}
}
// assign and done.
arc.s = s;
arc.e = e;
arc.r = r;
return arc;
},
numberSort: function(a, b) {
return a - b;
}
};
module.exports = utils;
})();

View File

@@ -1,6 +1,6 @@
import { enrich } from "../lib/enrich.js";
import { Point } from "./types/point.js";
import { Bezier } from "./types/bezier/base.js";
import { Bezier } from "./types/bezier.js";
import { Vector } from "./types/vector.js";
import { Shape } from "./util/shape.js";
import { BaseAPI } from "./base-api.js";
@@ -38,7 +38,7 @@ class GraphicsAPI extends BaseAPI {
super.onMouseDown(evt);
for (let i = 0, e = this.moveable.length, p; i < e; i++) {
p = this.moveable[i];
if (p.dist(this.cursor) <= 5) {
if (new Vector(p).dist(this.cursor) <= 5) {
this.currentPoint = p;
break;
}
@@ -53,7 +53,7 @@ class GraphicsAPI extends BaseAPI {
} else {
for (let i = 0, e = this.moveable.length, p; i < e; i++) {
p = this.moveable[i];
if (p.dist(this.cursor) <= 5) {
if (new Vector(p).dist(this.cursor) <= 5) {
this.setCursor(this.HAND);
return; // NOTE: this is a return, not a break.
}
@@ -175,26 +175,26 @@ class GraphicsAPI extends BaseAPI {
/**
* Draw a Point (or {x,y,z?} conformant) object on the canvas
*/
point(point) {
point.draw(this.ctx);
point(x, y) {
this.circle(x, y, 5);
}
/**
* Draw a line between two Points
*/
line(p1, p2) {
line(x1, y1, x2, y2) {
this.ctx.beginPath();
this.ctx.moveTo(p1.x + 0.5, p1.y + 0.5);
this.ctx.lineTo(p2.x + 0.5, p2.y + 0.5);
this.ctx.moveTo(x1 + 0.5, y1 + 0.5);
this.ctx.lineTo(x2 + 0.5, y2 + 0.5);
this.ctx.stroke();
}
/**
* Draw a circle around a Point
*/
circle(p, r) {
circle(x, y, r) {
this.ctx.beginPath();
this.ctx.arc(p.x + 0.5, p.y + 0.5, r, 0, this.TAU);
this.ctx.arc(x + 0.5, y + 0.5, r, 0, this.TAU);
this.ctx.fill();
this.ctx.stroke();
}
@@ -202,16 +202,20 @@ class GraphicsAPI extends BaseAPI {
/**
* Draw text on the canvas
*/
text(str, p) {
this.ctx.fillText(str, p.x + 0.5, p.y + 0.5);
text(str, x, y) {
if (y === undefined) {
y = x.y;
x = x.x;
}
this.ctx.fillText(str, x + 0.5, y + 0.5);
}
/**
* Draw a rectangle start with {p} in the upper left
*/
rect(p, w, h) {
this.ctx.fillRect(p.x + 0.5, p.y + 0.5, w, h);
this.ctx.strokeRect(p.x + 0.5, p.y + 0.5, w, h);
rect(x, y, w, h) {
this.ctx.fillRect(x + 0.5, y + 0.5, w, h);
this.ctx.strokeRect(x + 0.5, y + 0.5, w, h);
}
/**
@@ -231,8 +235,8 @@ class GraphicsAPI extends BaseAPI {
/**
* Add a plain point to the current shape
*/
vertex(p) {
this.currentShape.vertex(p);
vertex(x, y) {
this.currentShape.vertex({ x, y});
}
/**
@@ -240,8 +244,8 @@ class GraphicsAPI extends BaseAPI {
*/
end() {
this.ctx.beginPath();
let first = this.currentShape.first;
this.ctx.moveTo(first.x, first.y);
let {x, y} = this.currentShape.first;
this.ctx.moveTo(x, y);
this.currentShape.segments.forEach((s) =>
this[`draw${s.type}`](this.ctx, s.points, s.factor)
);
@@ -264,51 +268,6 @@ class GraphicsAPI extends BaseAPI {
this.end();
}
/**
* Polygon draw function
*/
drawPolygon(ctx, points) {
points.forEach((p) => ctx.lineTo(p.x, p.y));
}
/**
* Curve draw function, which draws a CR curve as a series of Beziers
*/
drawCatmullRom(ctx, points, f) {
// invent a virtual first and last point
const f0 = points[0],
f1 = points[1],
fn = f0.reflect(f1),
l1 = points[points.length - 2],
l0 = points[points.length - 1],
ln = l0.reflect(l1),
cpoints = [fn, ...points, ln];
// four point sliding window over the segment
for (let i = 0, e = cpoints.length - 3; i < e; i++) {
let [c1, c2, c3, c4] = cpoints.slice(i, i + 4);
let p2 = {
x: c2.x + (c3.x - c1.x) / (6 * f),
y: c2.y + (c3.y - c1.y) / (6 * f),
};
let p3 = {
x: c3.x - (c4.x - c2.x) / (6 * f),
y: c3.y - (c4.y - c2.y) / (6 * f),
};
ctx.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, c3.x, c3.y);
}
}
/**
* Curve draw function, which assumes Bezier coordinates
*/
drawBezier(ctx, points) {
for (let i = 0, e = points.length; i < e; i += 3) {
let [p1, p2, p3] = points.slice(i, i + 3);
ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
}
/**
* convenient grid drawing function
*/
@@ -320,8 +279,6 @@ class GraphicsAPI extends BaseAPI {
this.line({ x: 0, y }, { x: this.width, y });
}
}
// TODO: add in transform functions (translate, rotate, scale, skew)
}
export { GraphicsAPI, Bezier, Point };
export { GraphicsAPI, Bezier, Vector };

View File

@@ -1,70 +1,21 @@
import { Point } from "./point.js";
function compute(t, a, b, c, d) {
let mt = 1 - t,
t2 = t * t,
t3 = t2 * t,
mt2 = mt * mt,
mt3 = mt2 * mt;
return a * mt3 + 3 * b * mt2 * t + 3 * c * mt * t2 + d * t3;
}
function computeDerivative(t, a, b, c, d) {
let mt = 1 - t,
t2 = t * t,
mt2 = mt * mt,
u = 3 * (a - b),
v = 3 * (b - c),
w = 3 * (c - d);
return u * mt2 + 2 * v * mt * t + w * t2;
}
import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
/**
* A canvas-aware Bezier curve class
*/
class Bezier {
class Bezier extends Original {
static defaultQuadratic(apiInstance) {
return new Bezier(apiInstance, 70,250, 20,110, 220,60);
}
static defaultCubic(apiInstance) {
return new Bezier(apiInstance, 110,150, 25,190, 210,250, 210,30);
}
constructor(apiInstance, ...coords) {
if (coords.length === 8) {
this.points = [
new Point(coords[0], coords[1]),
new Point(coords[2], coords[3]),
new Point(coords[4], coords[5]),
new Point(coords[6], coords[7]),
];
}
super(...coords);
this.api = apiInstance;
this.ctx = apiInstance.ctx;
this.update();
}
update() {
this.buildLUT(25);
}
buildLUT(n) {
this.lut = [];
for (let i = 0; i <= n; i++) {
this.lut[i] = this.get(i / n);
}
}
get(t) {
let p = this.points;
let ret = new Point(
compute(t, p[0].x, p[1].x, p[2].x, p[3].x),
compute(t, p[0].y, p[1].y, p[2].y, p[3].y)
);
ret.t = t;
return ret;
}
getDerivative(t) {
let p = this.points;
let ret = new Point(
computeDerivative(t, p[0].x, p[1].x, p[2].x, p[3].x),
computeDerivative(t, p[0].y, p[1].y, p[2].y, p[3].y)
);
ret.t = t;
return ret;
}
getPointNear(point, d = 5) {
@@ -127,25 +78,33 @@ class Bezier {
ctx.strokeStyle = `#333`;
ctx.beginPath();
ctx.moveTo(p[0].x, p[0].y);
ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y);
if (p[3]) {
ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y);
} else {
ctx.quadraticCurveTo(p[1].x, p[1].y, p[2].x, p[2].y);
}
ctx.stroke();
ctx.restoreStyle();
}
drawPoints() {
const colors = [`red`, `green`, `blue`, `yellow`];
const api = this.api;
const ctx = this.ctx;
ctx.cacheStyle();
ctx.lineWidth = 2;
ctx.strokeStyle = `#999`;
const colors = [`red`, `green`, `blue`, `yellow`];
this.points.forEach((p, i) => {
ctx.fillStyle = colors[i];
p.draw(ctx);
api.setFill(colors[i % colors.length]);
api.circle(p.x, p.y, 5);
api.setFill(`black`);
api.text(`(${p.x},${p.y})`, p.x + 10, p.y + 10);
});
ctx.restoreStyle();
}
drawSkeleton() {
drawSkeleton(t = false) {
const ctx = this.ctx;
ctx.cacheStyle();
const p = this.points;
@@ -154,22 +113,15 @@ class Bezier {
ctx.moveTo(p[0].x, p[0].y);
ctx.lineTo(p[1].x, p[1].y);
ctx.lineTo(p[2].x, p[2].y);
ctx.lineTo(p[3].x, p[3].y);
ctx.stroke();
ctx.restoreStyle();
}
drawNormals() {
const ctx = this.ctx;
ctx.cacheStyle();
this.lut.forEach((p) => {
let tp = this.getDerivative(p.t).normalize(20);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x - tp.y, p.y + tp.x);
ctx.strokeStyle = `#CC00FFCC`;
ctx.stroke();
});
if (p[3]) {
ctx.lineTo(p[3].x, p[3].y);
if (t !== false) {
// TODO: additional cubic struts
// ... code goes here ...
}
}
ctx.stroke();
ctx.restoreStyle();
}
}

View File

@@ -1,40 +0,0 @@
import { Point } from "../point.js";
import { Quadratic } from "./bezier-quadratic.js";
import { Cubic } from "./bezier-cubic.js";
class Bezier {
static create(apiInstance, ...points) {
let coords = [];
if (points.length === 9 || points.length === 12) {
for(let i=0, e=points.length; i<e; i += 3) {
coords.push(new Point(points[i], points[i+1], points[i+2]));
}
}
if (points.length === 6 || points.length === 8) {
for(let i=0, e=points.length; i<e; i += 2) {
coords.push(new Point(points[i], points[i+1]));
}
}
if (coords.length === 3) {
return new Quadratic(apiInstance, coords);
}
if (coords.length === 4) {
return new Cubic(apiInstance, coords);
}
throw new Error(`Cannot create a Bezier curve for ${points.length} values`);
}
static defaultQuadratic(apiInstance) {
return this.create(apiInstance, 70,250, 20,110, 220,60);
}
static defaultCubic(apiInstance) {
return this.create(apiInstance, 110,150, 25,190, 210,250, 210,30);
}
}
export { Bezier };

View File

@@ -1,47 +0,0 @@
import { Bezier, Point } from "./bezier.js";
function compute(t, a, b, c, d) {
let mt = 1 - t,
t2 = t * t,
t3 = t2 * t,
mt2 = mt * mt,
mt3 = mt2 * mt;
return a * mt3 + 3 * b * mt2 * t + 3 * c * mt * t2 + d * t3;
}
function computeDerivative(t, a, b, c, d) {
let mt = 1 - t,
t2 = t * t,
mt2 = mt * mt,
u = 3 * (a - b),
v = 3 * (b - c),
w = 3 * (c - d);
return u * mt2 + 2 * v * mt * t + w * t2;
}
/**
* A canvas-aware Bezier curve class
*/
class Cubic extends Bezier {
get(t) {
let p = this.points;
let ret = new Point(
compute(t, p[0].x, p[1].x, p[2].x, p[3].x),
compute(t, p[0].y, p[1].y, p[2].y, p[3].y)
);
ret.t = t;
return ret;
}
getDerivative(t) {
let p = this.points;
let ret = new Point(
computeDerivative(t, p[0].x, p[1].x, p[2].x, p[3].x),
computeDerivative(t, p[0].y, p[1].y, p[2].y, p[3].y)
);
ret.t = t;
return ret;
}
}
export { Cubic };

View File

@@ -1,39 +0,0 @@
import { Bezier, Point } from "./bezier.js";
function compute(t, a, b, c) {
let mt = 1 - t;
return a * (mt * mt) + 2 * b * mt * t + c * (t * t);
}
function computeDerivative(t, a, b, c) {
let u = 2 * (a - b),
v = 2 * (b - c);
return u * (1-t) + v * t;
}
/**
* A canvas-aware Bezier curve class
*/
class Quadratic extends Bezier {
get(t) {
let p = this.points;
let ret = new Point(
compute(t, p[0].x, p[1].x, p[2].x),
compute(t, p[0].y, p[1].y, p[2].y)
);
ret.t = t;
return ret;
}
getDerivative(t) {
let p = this.points;
let ret = new Point(
computeDerivative(t, p[0].x, p[1].x, p[2].x),
computeDerivative(t, p[0].y, p[1].y, p[2].y)
);
ret.t = t;
return ret;
}
}
export { Quadratic };

View File

@@ -1,134 +0,0 @@
import { Point } from "../point.js";
/**
* A canvas-aware Bezier curve class
*/
class Bezier {
constructor(apiInstance, points) {
this.points = points;
this.ctx = apiInstance.ctx;
this.update();
}
update() {
this.buildLUT(25);
}
buildLUT(n) {
this.lut = [];
for (let i = 0; i <= n; i++) {
this.lut[i] = this.get(i / n);
}
}
getPointNear(point, d = 5) {
const { x, y } = point;
const p = this.points;
for (let i = 0, e = p.length; i < e; i++) {
let dx = Math.abs(p[i].x - x);
let dy = Math.abs(p[i].y - y);
if ((dx * dx + dy * dy) ** 0.5 <= d) {
return p[i];
}
}
}
getProjectionPoint(point) {
const { x, y } = point;
// project this point onto the curve and return _that_ point
const n = this.lut.length - 1,
p = this.points;
let d,
closest,
smallestDistance = Number.MAX_SAFE_INTEGER;
// coarse check
this.lut.forEach((p, i) => {
d = p.dist(x, y);
if (d < smallestDistance) {
smallestDistance = d;
p.t = i / n;
closest = p;
}
});
// fine check
for (let o = -0.1, t, np, st = closest.t; o <= 0.1; o += 0.005) {
t = st + o;
if (t < 0) continue;
if (t > 1) continue;
np = this.get(t);
d = np.dist(x, y);
if (d < smallestDistance) {
smallestDistance = d;
closest = np;
closest.t = t;
}
}
return closest;
}
drawCurve() {
const ctx = this.ctx;
const p = this.points;
ctx.cacheStyle();
ctx.lineWidth = 2;
ctx.strokeStyle = `#333`;
ctx.beginPath();
ctx.moveTo(p[0].x, p[0].y);
if (!p[3]) {
ctx.quadraticCurveTo(p[1].x, p[1].y, p[2].x, p[2].y);
} else {
ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y);
}
ctx.stroke();
ctx.restoreStyle();
}
drawPoints() {
const ctx = this.ctx;
ctx.cacheStyle();
ctx.lineWidth = 2;
ctx.strokeStyle = `#999`;
const colors = [`red`, `green`, `blue`, `yellow`];
this.points.forEach((p, i) => {
ctx.fillStyle = colors[i];
p.draw(ctx);
});
ctx.restoreStyle();
}
drawSkeleton() {
const ctx = this.ctx;
ctx.cacheStyle();
const p = this.points;
ctx.strokeStyle = `#555`;
ctx.beginPath();
ctx.moveTo(p[0].x, p[0].y);
ctx.lineTo(p[1].x, p[1].y);
ctx.lineTo(p[2].x, p[2].y);
if (p[3]) {
ctx.lineTo(p[3].x, p[3].y);
}
ctx.stroke();
ctx.restoreStyle();
}
drawNormals() {
const ctx = this.ctx;
ctx.cacheStyle();
this.lut.forEach((p) => {
let tp = this.getDerivative(p.t).normalize(20);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x - tp.y, p.y + tp.x);
ctx.strokeStyle = `#CC00FFCC`;
ctx.stroke();
});
ctx.restoreStyle();
}
}
export { Bezier, Point }

View File

@@ -1,85 +0,0 @@
class Point {
constructor(x, y, z) {
this.x = x;
this.y = y;
if (z !== undefined) {
this.z = z;
}
}
draw(ctx) {
ctx.cacheStyle();
ctx.beginPath();
ctx.arc(this.x, this.y, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.fillStyle = `black`;
ctx.fillText(`(${this.x},${this.y})`, this.x + 10.5, this.y + 10.5);
ctx.restoreStyle();
}
dist(other, y, z = 0) {
if (y !== undefined) other = { x: other, y, z };
let sum = 0;
sum += (this.x - other.x) ** 2;
sum += (this.y - other.y) ** 2;
let z1 = this.z !== undefined ? this.z : 0;
let z2 = other.z !== undefined ? other.z : 0;
sum += (z1 - z2) ** 2;
return sum ** 0.5;
}
normalize(f) {
let mag = this.dist(0, 0, 0);
return new Point(
(f * this.x) / mag,
(f * this.y) / mag,
(f * this.z) / mag
);
}
getAngle() {
return -Math.atan2(this.y, this.x);
}
reflect(other) {
let p = new Point(
other.x - this.x,
other.y - this.y
);
if (other.z !== undefined) {
p.z = other.z
if (this.z !== undefined) {
p.z -= this.z;
}
}
return this.subtract(p);
}
add(other) {
let p = new Point(this.x + other.x, this.y + other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z += other.z;
}
}
return p;
}
subtract(other) {
let p = new Point(this.x - other.x, this.y - other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z -= other.z;
}
}
return p;
}
scale(f = 1) {
if (f === 0) {
return new Point(0, 0, this.z === undefined ? undefined : 0);
}
let p = new Point(this.x * f, this.y * f);
if (this.z !== undefined) {
p.z = this.z * f;
}
return p;
}
}
export { Point };

View File

@@ -0,0 +1,80 @@
class Vector {
constructor(x, y, z) {
if (arguments.length === 1) {
z = x.z;
y = x.y;
x = x.x;
}
this.x = x;
this.y = y;
if (z !== undefined) {
this.z = z;
}
}
dist(other, y, z = 0) {
if (y !== undefined) other = { x: other, y, z };
let sum = 0;
sum += (this.x - other.x) ** 2;
sum += (this.y - other.y) ** 2;
let z1 = this.z ?? 0;
let z2 = other.z ?? 0;
sum += (z1 - z2) ** 2;
return sum ** 0.5;
}
normalize(f) {
let mag = this.dist(0, 0, 0);
return new Vector(
(f * this.x) / mag,
(f * this.y) / mag,
(f * this.z) / mag
);
}
getAngle() {
return -Math.atan2(this.y, this.x);
}
reflect(other) {
let p = new Vector(
other.x - this.x,
other.y - this.y
);
if (other.z !== undefined) {
p.z = other.z
if (this.z !== undefined) {
p.z -= this.z;
}
}
return this.subtract(p);
}
add(other) {
let p = new Vector(this.x + other.x, this.y + other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z += other.z;
}
}
return p;
}
subtract(other) {
let p = new Vector(this.x - other.x, this.y - other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z -= other.z;
}
}
return p;
}
scale(f = 1) {
if (f === 0) {
return new Vector(0, 0, this.z === undefined ? undefined : 0);
}
let p = new Vector(this.x * f, this.y * f);
if (this.z !== undefined) {
p.z = this.z * f;
}
return p;
}
}
export { Vector };

View File

@@ -147,7 +147,7 @@ class GraphicsElement extends CustomElement {
const height = this.getAttribute(`height`, 200);
this.code = `
import { GraphicsAPI, Bezier, Point } from "${MODULE_PATH}/api/graphics-api.js";
import { GraphicsAPI, Bezier, Vector } from "${MODULE_PATH}/api/graphics-api.js";
${globalCode}

View File

@@ -0,0 +1,963 @@
/**
A javascript Bezier curve library by Pomax.
Based on http://pomax.github.io/bezierinfo
This code is MIT licensed.
**/
import { utils } from "./utils.js";
import { PolyBezier } from "./poly-bezier.js";
import { convertPath } from "./svg-to-beziers.js";
// math-inlining.
const { abs, min, max, cos, sin, acos, sqrt } = Math;
const pi = Math.PI;
// a zero coordinate, which is surprisingly useful
const ZERO = { x: 0, y: 0, z: 0 };
// TODO: figure out where this function goes, it has no reason to exist on its lonesome.
function getABC(n, S, B, E, t) {
if (typeof t === "undefined") {
t = 0.5;
}
const u = utils.projectionratio(t, n),
um = 1 - u,
C = {
x: u * S.x + um * E.x,
y: u * S.y + um * E.y,
},
s = utils.abcratio(t, n),
A = {
x: B.x + (B.x - C.x) / s,
y: B.y + (B.y - C.y) / s,
};
return { A: A, B: B, C: C };
}
/**
* Bezier curve constructor.
*
* ...docs pending...
*/
class Bezier {
constructor(coords) {
let args =
coords && coords.forEach ? coords : Array.from(arguments).slice();
let coordlen = false;
if (typeof args[0] === "object") {
coordlen = args.length;
const newargs = [];
args.forEach(function (point) {
["x", "y", "z"].forEach(function (d) {
if (typeof point[d] !== "undefined") {
newargs.push(point[d]);
}
});
});
args = newargs;
}
let higher = false;
const len = args.length;
if (coordlen) {
if (coordlen > 4) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
higher = true;
}
} else {
if (len !== 6 && len !== 8 && len !== 9 && len !== 12) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
}
}
const _3d = (this._3d =
(!higher && (len === 9 || len === 12)) ||
(coords && coords[0] && typeof coords[0].z !== "undefined"));
const points = (this.points = []);
for (let idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) {
var point = {
x: args[idx],
y: args[idx + 1],
};
if (_3d) {
point.z = args[idx + 2];
}
points.push(point);
}
const order = (this.order = points.length - 1);
const dims = (this.dims = ["x", "y"]);
if (_3d) dims.push("z");
this.dimlen = dims.length;
const aligned = utils.align(points, { p1: points[0], p2: points[order] });
this._linear = !aligned.some((p) => abs(p.y) > 0.0001);
this._lut = [];
this._t1 = 0;
this._t2 = 1;
this.update();
}
static SVGtoBeziers = function (d) {
return convertPath(Bezier, d);
};
static quadraticFromPoints(p1, p2, p3, t) {
if (typeof t === "undefined") {
t = 0.5;
}
// shortcuts, although they're really dumb
if (t === 0) {
return new Bezier(p2, p2, p3);
}
if (t === 1) {
return new Bezier(p1, p2, p2);
}
// real fitting.
const abc = getABC(2, p1, p2, p3, t);
return new Bezier(p1, abc.A, p3);
}
static cubicFromPoints(S, B, E, t, d1) {
if (typeof t === "undefined") {
t = 0.5;
}
const abc = getABC(3, S, B, E, t);
if (typeof d1 === "undefined") {
d1 = utils.dist(B, abc.C);
}
const d2 = (d1 * (1 - t)) / t;
const selen = utils.dist(S, E),
lx = (E.x - S.x) / selen,
ly = (E.y - S.y) / selen,
bx1 = d1 * lx,
by1 = d1 * ly,
bx2 = d2 * lx,
by2 = d2 * ly;
// derivation of new hull coordinates
const e1 = { x: B.x - bx1, y: B.y - by1 },
e2 = { x: B.x + bx2, y: B.y + by2 },
A = abc.A,
v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) },
v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t },
nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t },
nc2 = {
x: E.x + (v2.x - E.x) / (1 - t),
y: E.y + (v2.y - E.y) / (1 - t),
};
// ...done
return new Bezier(S, nc1, nc2, E);
}
static getUtils() {
return utils;
}
getUtils() {
return Bezier.getUtils();
}
static get PolyBezier() {
return PolyBezier;
}
valueOf() {
return this.toString();
}
toString() {
return utils.pointsToString(this.points);
}
toSVG() {
if (this._3d) return false;
const p = this.points,
x = p[0].x,
y = p[0].y,
s = ["M", x, y, this.order === 2 ? "Q" : "C"];
for (let i = 1, last = p.length; i < last; i++) {
s.push(p[i].x);
s.push(p[i].y);
}
return s.join(" ");
}
setRatios(ratios) {
if (ratios.length !== this.points.length) {
throw new Error("incorrect number of ratio values");
}
this.ratios = ratios;
this._lut = []; // invalidate any precomputed LUT
}
verify() {
const print = this.coordDigest();
if (print !== this._print) {
this._print = print;
this.update();
}
}
coordDigest() {
return this.points
.map(function (c, pos) {
return "" + pos + c.x + c.y + (c.z ? c.z : 0);
})
.join("");
}
update() {
// invalidate any precomputed LUT
this._lut = [];
this.dpoints = utils.derive(this.points, this._3d);
this.computedirection();
}
computedirection() {
const points = this.points;
const angle = utils.angle(points[0], points[this.order], points[1]);
this.clockwise = angle > 0;
}
length() {
return utils.length(this.derivative.bind(this));
}
getLUT(steps) {
this.verify();
steps = steps || 100;
if (this._lut.length === steps) {
return this._lut;
}
this._lut = [];
// We want a range from 0 to 1 inclusive, so
// we decrement and then use <= rather than <:
steps--;
for (let t = 0; t <= steps; t++) {
this._lut.push(this.compute(t / steps));
}
return this._lut;
}
on(point, error) {
error = error || 5;
const lut = this.getLUT(),
hits = [];
for (let i = 0, c, t = 0; i < lut.length; i++) {
c = lut[i];
if (utils.dist(c, point) < error) {
hits.push(c);
t += i / lut.length;
}
}
if (!hits.length) return false;
return (t /= hits.length);
}
project(point) {
// step 1: coarse check
const LUT = this.getLUT(),
l = LUT.length - 1,
closest = utils.closest(LUT, point),
mpos = closest.mpos,
t1 = (mpos - 1) / l,
t2 = (mpos + 1) / l,
step = 0.1 / l;
// step 2: fine check
let mdist = closest.mdist,
t = t1,
ft = t,
p;
mdist += 1;
for (let d; t < t2 + step; t += step) {
p = this.compute(t);
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
ft = t;
}
}
p = this.compute(ft);
p.t = ft;
p.d = mdist;
return p;
}
get(t) {
return this.compute(t);
}
point(idx) {
return this.points[idx];
}
compute(t) {
if (this.ratios) {
return utils.computeWithRatios(t, this.points, this.ratios, this._3d);
}
return utils.compute(t, this.points, this._3d, this.ratios);
}
raise() {
const p = this.points,
np = [p[0]],
k = p.length;
for (let i = 1, pi, pim; i < k; i++) {
pi = p[i];
pim = p[i - 1];
np[i] = {
x: ((k - i) / k) * pi.x + (i / k) * pim.x,
y: ((k - i) / k) * pi.y + (i / k) * pim.y,
};
}
np[k] = p[k - 1];
return new Bezier(np);
}
derivative(t) {
const mt = 1 - t;
let a,
b,
c = 0,
p = this.dpoints[0];
if (this.order === 2) {
p = [p[0], p[1], ZERO];
a = mt;
b = t;
}
if (this.order === 3) {
a = mt * mt;
b = mt * t * 2;
c = t * t;
}
const ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x,
y: a * p[0].y + b * p[1].y + c * p[2].y,
};
if (this._3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z;
}
return ret;
}
curvature(t) {
return utils.curvature(t, this.points, this._3d);
}
inflections() {
return utils.inflections(this.points);
}
normal(t) {
return this._3d ? this.__normal3(t) : this.__normal2(t);
}
__normal2(t) {
const d = this.derivative(t);
const q = sqrt(d.x * d.x + d.y * d.y);
return { x: -d.y / q, y: d.x / q };
}
__normal3(t) {
// see http://stackoverflow.com/questions/25453159
const r1 = this.derivative(t),
r2 = this.derivative(t + 0.01),
q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z),
q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z);
r1.x /= q1;
r1.y /= q1;
r1.z /= q1;
r2.x /= q2;
r2.y /= q2;
r2.z /= q2;
// cross product
const c = {
x: r2.y * r1.z - r2.z * r1.y,
y: r2.z * r1.x - r2.x * r1.z,
z: r2.x * r1.y - r2.y * r1.x,
};
const m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z);
c.x /= m;
c.y /= m;
c.z /= m;
// rotation matrix
const R = [
c.x * c.x,
c.x * c.y - c.z,
c.x * c.z + c.y,
c.x * c.y + c.z,
c.y * c.y,
c.y * c.z - c.x,
c.x * c.z - c.y,
c.y * c.z + c.x,
c.z * c.z,
];
// normal vector:
const n = {
x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z,
y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z,
z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z,
};
return n;
}
hull(t) {
let p = this.points,
_p = [],
q = [],
idx = 0;
q[idx++] = p[0];
q[idx++] = p[1];
q[idx++] = p[2];
if (this.order === 3) {
q[idx++] = p[3];
}
// we lerp between all points at each iteration, until we have 1 point left.
while (p.length > 1) {
_p = [];
for (let i = 0, pt, l = p.length - 1; i < l; i++) {
pt = utils.lerp(t, p[i], p[i + 1]);
q[idx++] = pt;
_p.push(pt);
}
p = _p;
}
return q;
}
split(t1, t2) {
// shortcuts
if (t1 === 0 && !!t2) {
return this.split(t2).left;
}
if (t2 === 1) {
return this.split(t1).right;
}
// no shortcut: use "de Casteljau" iteration.
const q = this.hull(t1);
const result = {
left:
this.order === 2
? new Bezier([q[0], q[3], q[5]])
: new Bezier([q[0], q[4], q[7], q[9]]),
right:
this.order === 2
? new Bezier([q[5], q[4], q[2]])
: new Bezier([q[9], q[8], q[6], q[3]]),
span: q,
};
// make sure we bind _t1/_t2 information!
result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2);
result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2);
// if we have no t2, we're done
if (!t2) {
return result;
}
// if we have a t2, split again:
t2 = utils.map(t2, t1, 1, 0, 1);
return result.right.split(t2).left;
}
extrema() {
const result = {};
let roots = [];
this.dims.forEach(
function (dim) {
let mfn = function (v) {
return v[dim];
};
let p = this.dpoints[0].map(mfn);
result[dim] = utils.droots(p);
if (this.order === 3) {
p = this.dpoints[1].map(mfn);
result[dim] = result[dim].concat(utils.droots(p));
}
result[dim] = result[dim].filter(function (t) {
return t >= 0 && t <= 1;
});
roots = roots.concat(result[dim].sort(utils.numberSort));
}.bind(this)
);
result.values = roots.sort(utils.numberSort).filter(function (v, idx) {
return roots.indexOf(v) === idx;
});
return result;
}
bbox() {
const extrema = this.extrema(),
result = {};
this.dims.forEach(
function (d) {
result[d] = utils.getminmax(this, d, extrema[d]);
}.bind(this)
);
return result;
}
overlaps(curve) {
const lbbox = this.bbox(),
tbbox = curve.bbox();
return utils.bboxoverlap(lbbox, tbbox);
}
offset(t, d) {
if (typeof d !== "undefined") {
const c = this.get(t),
n = this.normal(t);
const ret = {
c: c,
n: n,
x: c.x + n.x * d,
y: c.y + n.y * d,
};
if (this._3d) {
ret.z = c.z + n.z * d;
}
return ret;
}
if (this._linear) {
const nv = this.normal(0),
coords = this.points.map(function (p) {
const ret = {
x: p.x + t * nv.x,
y: p.y + t * nv.y,
};
if (p.z && nv.z) {
ret.z = p.z + t * nv.z;
}
return ret;
});
return [new Bezier(coords)];
}
return this.reduce().map(function (s) {
if (s._linear) {
return s.offset(t)[0];
}
return s.scale(t);
});
}
simple() {
if (this.order === 3) {
const a1 = utils.angle(this.points[0], this.points[3], this.points[1]);
const a2 = utils.angle(this.points[0], this.points[3], this.points[2]);
if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false;
}
const n1 = this.normal(0);
const n2 = this.normal(1);
let s = n1.x * n2.x + n1.y * n2.y;
if (this._3d) {
s += n1.z * n2.z;
}
return abs(acos(s)) < pi / 3;
}
reduce() {
// TODO: examine these var types in more detail...
let i,
t1 = 0,
t2 = 0,
step = 0.01,
segment,
pass1 = [],
pass2 = [];
// first pass: split on extrema
let extrema = this.extrema().values;
if (extrema.indexOf(0) === -1) {
extrema = [0].concat(extrema);
}
if (extrema.indexOf(1) === -1) {
extrema.push(1);
}
for (t1 = extrema[0], i = 1; i < extrema.length; i++) {
t2 = extrema[i];
segment = this.split(t1, t2);
segment._t1 = t1;
segment._t2 = t2;
pass1.push(segment);
t1 = t2;
}
// second pass: further reduce these segments to simple segments
pass1.forEach(function (p1) {
t1 = 0;
t2 = 0;
while (t2 <= 1) {
for (t2 = t1 + step; t2 <= 1 + step; t2 += step) {
segment = p1.split(t1, t2);
if (!segment.simple()) {
t2 -= step;
if (abs(t1 - t2) < step) {
// we can never form a reduction
return [];
}
segment = p1.split(t1, t2);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2);
pass2.push(segment);
t1 = t2;
break;
}
}
}
if (t1 < 1) {
segment = p1.split(t1, 1);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = p1._t2;
pass2.push(segment);
}
});
return pass2;
}
scale(d) {
const order = this.order;
let distanceFn = false;
if (typeof d === "function") {
distanceFn = d;
}
if (distanceFn && order === 2) {
return this.raise().scale(distanceFn);
}
// TODO: add special handling for degenerate (=linear) curves.
const clockwise = this.clockwise;
const r1 = distanceFn ? distanceFn(0) : d;
const r2 = distanceFn ? distanceFn(1) : d;
const v = [this.offset(0, 10), this.offset(1, 10)];
const points = this.points;
const np = [];
const o = utils.lli4(v[0], v[0].c, v[1], v[1].c);
if (!o) {
throw new Error("cannot scale this curve. Try reducing it first.");
}
// move all points by distance 'd' wrt the origin 'o'
// move end points by fixed distance along normal.
[0, 1].forEach(function (t) {
const p = (np[t * order] = utils.copy(points[t * order]));
p.x += (t ? r2 : r1) * v[t].n.x;
p.y += (t ? r2 : r1) * v[t].n.y;
});
if (!distanceFn) {
// move control points to lie on the intersection of the offset
// derivative vector, and the origin-through-control vector
[0, 1].forEach((t) => {
if (order === 2 && !!t) return;
const p = np[t * order];
const d = this.derivative(t);
const p2 = { x: p.x + d.x, y: p.y + d.y };
np[t + 1] = utils.lli4(p, p2, o, points[t + 1]);
});
return new Bezier(np);
}
// move control points by "however much necessary to
// ensure the correct tangent to endpoint".
[0, 1].forEach(function (t) {
if (order === 2 && !!t) return;
var p = points[t + 1];
var ov = {
x: p.x - o.x,
y: p.y - o.y,
};
var rc = distanceFn ? distanceFn((t + 1) / order) : d;
if (distanceFn && !clockwise) rc = -rc;
var m = sqrt(ov.x * ov.x + ov.y * ov.y);
ov.x /= m;
ov.y /= m;
np[t + 1] = {
x: p.x + rc * ov.x,
y: p.y + rc * ov.y,
};
});
return new Bezier(np);
}
outline(d1, d2, d3, d4) {
d2 = typeof d2 === "undefined" ? d1 : d2;
const reduced = this.reduce(),
len = reduced.length,
fcurves = [],
bcurves = [];
let p,
alen = 0,
tlen = this.length();
const graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined";
function linearDistanceFunction(s, e, tlen, alen, slen) {
return function (v) {
const f1 = alen / tlen,
f2 = (alen + slen) / tlen,
d = e - s;
return utils.map(v, 0, 1, s + f1 * d, s + f2 * d);
};
}
// form curve oulines
reduced.forEach(function (segment) {
slen = segment.length();
if (graduated) {
fcurves.push(
segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen))
);
bcurves.push(
segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen))
);
} else {
fcurves.push(segment.scale(d1));
bcurves.push(segment.scale(-d2));
}
alen += slen;
});
// reverse the "return" outline
bcurves = bcurves
.map(function (s) {
p = s.points;
if (p[3]) {
s.points = [p[3], p[2], p[1], p[0]];
} else {
s.points = [p[2], p[1], p[0]];
}
return s;
})
.reverse();
// form the endcaps as lines
const fs = fcurves[0].points[0],
fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1],
bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1],
be = bcurves[0].points[0],
ls = utils.makeline(bs, fs),
le = utils.makeline(fe, be),
segments = [ls].concat(fcurves).concat([le]).concat(bcurves),
slen = segments.length;
return new PolyBezier(segments);
}
outlineshapes(d1, d2, curveIntersectionThreshold) {
d2 = d2 || d1;
const outline = this.outline(d1, d2).curves;
const shapes = [];
for (let i = 1, len = outline.length; i < len / 2; i++) {
const shape = utils.makeshape(
outline[i],
outline[len - i],
curveIntersectionThreshold
);
shape.startcap.virtual = i > 1;
shape.endcap.virtual = i < len / 2 - 1;
shapes.push(shape);
}
return shapes;
}
intersects(curve, curveIntersectionThreshold) {
if (!curve) return this.selfintersects(curveIntersectionThreshold);
if (curve.p1 && curve.p2) {
return this.lineIntersects(curve);
}
if (curve instanceof Bezier) {
curve = curve.reduce();
}
return this.curveintersects(
this.reduce(),
curve,
curveIntersectionThreshold
);
}
lineIntersects(line) {
const mx = min(line.p1.x, line.p2.x),
my = min(line.p1.y, line.p2.y),
MX = max(line.p1.x, line.p2.x),
MY = max(line.p1.y, line.p2.y);
return utils.roots(this.points, line).filter((t) => {
var p = this.get(t);
return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY);
});
}
selfintersects(curveIntersectionThreshold) {
// "simple" curves cannot intersect with their direct
// neighbour, so for each segment X we check whether
// it intersects [0:x-2][x+2:last].
const reduced = this.reduce(),
len = reduced.length - 2,
results = [];
for (let i = 0, result, left, right; i < len; i++) {
left = reduced.slice(i, i + 1);
right = reduced.slice(i + 2);
result = this.curveintersects(left, right, curveIntersectionThreshold);
results = results.concat(result);
}
return results;
}
curveintersects(c1, c2, curveIntersectionThreshold) {
const pairs = [];
// step 1: pair off any overlapping segments
c1.forEach(function (l) {
c2.forEach(function (r) {
if (l.overlaps(r)) {
pairs.push({ left: l, right: r });
}
});
});
// step 2: for each pairing, run through the convergence algorithm.
let intersections = [];
pairs.forEach(function (pair) {
const result = utils.pairiteration(
pair.left,
pair.right,
curveIntersectionThreshold
);
if (result.length > 0) {
intersections = intersections.concat(result);
}
});
return intersections;
}
arcs(errorThreshold) {
errorThreshold = errorThreshold || 0.5;
return this._iterate(errorThreshold, []);
}
_error(pc, np1, s, e) {
const q = (e - s) / 4,
c1 = this.get(s + q),
c2 = this.get(e - q),
ref = utils.dist(pc, np1),
d1 = utils.dist(pc, c1),
d2 = utils.dist(pc, c2);
return abs(d1 - ref) + abs(d2 - ref);
}
_iterate(errorThreshold, circles) {
let t_s = 0,
t_e = 1,
safety;
// we do a binary search to find the "good `t` closest to no-longer-good"
do {
safety = 0;
// step 1: start with the maximum possible arc
t_e = 1;
// points:
let np1 = this.get(t_s),
np2,
np3,
arc,
prev_arc;
// booleans:
let curr_good = false,
prev_good = false,
done;
// numbers:
let t_m = t_e,
prev_e = 1,
step = 0;
// step 2: find the best possible arc
do {
prev_good = curr_good;
prev_arc = arc;
t_m = (t_s + t_e) / 2;
step++;
np2 = this.get(t_m);
np3 = this.get(t_e);
arc = utils.getccenter(np1, np2, np3);
//also save the t values
arc.interval = {
start: t_s,
end: t_e,
};
let error = this._error(arc, np1, t_s, t_e);
curr_good = error <= errorThreshold;
done = prev_good && !curr_good;
if (!done) prev_e = t_e;
// this arc is fine: we can move 'e' up to see if we can find a wider arc
if (curr_good) {
// if e is already at max, then we're done for this arc.
if (t_e >= 1) {
// make sure we cap at t=1
arc.interval.end = prev_e = 1;
prev_arc = arc;
// if we capped the arc segment to t=1 we also need to make sure that
// the arc's end angle is correct with respect to the bezier end point.
if (t_e > 1) {
let d = {
x: arc.x + arc.r * cos(arc.e),
y: arc.y + arc.r * sin(arc.e),
};
arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1));
}
break;
}
// if not, move it up by half the iteration distance
t_e = t_e + (t_e - t_s) / 2;
} else {
// this is a bad arc: we need to move 'e' down to find a good arc
t_e = t_m;
}
} while (!done && safety++ < 100);
if (safety >= 100) {
break;
}
// console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e);
prev_arc = prev_arc ? prev_arc : arc;
circles.push(prev_arc);
t_s = prev_e;
} while (t_e < 1);
return circles;
}
}
export { Bezier };

View File

@@ -3,7 +3,7 @@
* and full commands, rather than relative coordinates
* and/or shortcut commands.
*/
function normalizePath(d) {
export default function normalizePath(d) {
// preprocess "d" so that we have spaces between values
d = d
.replace(/,/g, " ") // replace commas with spaces
@@ -12,9 +12,10 @@ function normalizePath(d) {
.replace(/([a-zA-Z])/g, " $1 ");
// set up the variables used in this function
var instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"),
instructionLength = instructions.length,
i,
const instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"),
instructionLength = instructions.length;
let i,
instruction,
op,
lop,
@@ -29,6 +30,11 @@ function normalizePath(d) {
cy = 0,
cx2 = 0,
cy2 = 0,
rx = 0,
ry = 0,
xrot = 0,
lflag = 0,
sweep = 0,
normalized = "";
// we run through the instruction list starting at 1, not 0,
@@ -42,12 +48,9 @@ function normalizePath(d) {
// what are the arguments? note that we need to convert
// all strings into numbers, or + will do silly things.
args = instruction
.replace(op, "")
.trim()
.split(" ");
args = instruction.replace(op, "").trim().split(" ");
args = args
.filter(function(v) {
.filter(function (v) {
return v !== "";
})
.map(parseFloat);
@@ -81,11 +84,13 @@ function normalizePath(d) {
x = args[a];
y = args[a + 1];
}
normalized += ["L",x,y,''].join(" ");
normalized += "L " + x + " " + y + " ";
}
}
} else if (lop === "l") {
// lineto commands
}
// lineto commands
else if (lop === "l") {
for (a = 0; a < alen; a += 2) {
if (op === "l") {
x += args[a];
@@ -94,7 +99,7 @@ function normalizePath(d) {
x = args[a];
y = args[a + 1];
}
normalized += ["L",x,y,''].join(" ");
normalized += "L " + x + " " + y + " ";
}
} else if (lop === "h") {
for (a = 0; a < alen; a++) {
@@ -103,7 +108,7 @@ function normalizePath(d) {
} else {
x = args[a];
}
normalized += ["L",x,y,''].join(" ");
normalized += "L " + x + " " + y + " ";
}
} else if (lop === "v") {
for (a = 0; a < alen; a++) {
@@ -112,10 +117,12 @@ function normalizePath(d) {
} else {
y = args[a];
}
normalized += ["L",x,y,''].join(" ");
normalized += "L " + x + " " + y + " ";
}
} else if (lop === "q") {
// quadratic curveto commands
}
// quadratic curveto commands
else if (lop === "q") {
for (a = 0; a < alen; a += 4) {
if (op === "q") {
cx = x + args[a];
@@ -128,7 +135,7 @@ function normalizePath(d) {
x = args[a + 2];
y = args[a + 3];
}
normalized += ["Q",cx,cy,x,y,''].join(" ");
normalized += "Q " + cx + " " + cy + " " + x + " " + y + " ";
}
} else if (lop === "t") {
for (a = 0; a < alen; a += 2) {
@@ -143,10 +150,12 @@ function normalizePath(d) {
x = args[a];
y = args[a + 1];
}
normalized += ["Q",cx,cy,x,y,''].join(" ");
normalized += "Q " + cx + " " + cy + " " + x + " " + y + " ";
}
} else if (lop === "c") {
// cubic curveto commands
}
// cubic curveto commands
else if (lop === "c") {
for (a = 0; a < alen; a += 6) {
if (op === "c") {
cx = x + args[a];
@@ -163,7 +172,20 @@ function normalizePath(d) {
x = args[a + 4];
y = args[a + 5];
}
normalized += ["C",cx,cy,cx2,cy2,x,y,''].join(" ");
normalized +=
"C " +
cx +
" " +
cy +
" " +
cx2 +
" " +
cy2 +
" " +
x +
" " +
y +
" ";
}
} else if (lop === "s") {
for (a = 0; a < alen; a += 4) {
@@ -182,7 +204,57 @@ function normalizePath(d) {
x = args[a + 2];
y = args[a + 3];
}
normalized +=["C",cx,cy,cx2,cy2,x,y,''].join(" ");
normalized +=
"C " +
cx +
" " +
cy +
" " +
cx2 +
" " +
cy2 +
" " +
x +
" " +
y +
" ";
}
}
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
// a 25,25 -30 0, 1 50,-25
// arc command
else if (lop === "a") {
for (a = 0; a < alen; a += 7) {
rx = args[a];
ry = args[a + 1];
xrot = args[a + 2];
lflag = args[a + 3];
sweep = args[a + 4];
if (op === "a") {
x += args[a + 5];
y += args[a + 6];
} else {
x = args[a + 5];
y = args[a + 6];
}
normalized +=
"A " +
rx +
" " +
ry +
" " +
xrot +
" " +
lflag +
" " +
sweep +
" " +
x +
" " +
y +
" ";
}
} else if (lop === "z") {
normalized += "Z ";
@@ -193,5 +265,3 @@ function normalizePath(d) {
}
return normalized.trim();
}
module.exports = normalizePath;

View File

@@ -0,0 +1,70 @@
import { utils } from "./utils.js";
/**
* Poly Bezier
* @param {[type]} curves [description]
*/
class PolyBezier {
constructor(curves) {
this.curves = [];
this._3d = false;
if (!!curves) {
this.curves = curves;
this._3d = this.curves[0]._3d;
}
}
valueOf() {
return this.toString();
}
toString() {
return (
"[" +
this.curves
.map(function (curve) {
return utils.pointsToString(curve.points);
})
.join(", ") +
"]"
);
}
addCurve(curve) {
this.curves.push(curve);
this._3d = this._3d || curve._3d;
}
length() {
return this.curves
.map(function (v) {
return v.length();
})
.reduce(function (a, b) {
return a + b;
});
}
curve(idx) {
return this.curves[idx];
}
bbox() {
const c = this.curves;
var bbox = c[0].bbox();
for (var i = 1; i < c.length; i++) {
utils.expandbox(bbox, c[i].bbox());
}
return bbox;
}
offset(d) {
const offset = [];
this.curves.forEach(function (v) {
offset = offset.concat(v.offset(d));
});
return new PolyBezier(offset);
}
}
export { PolyBezier };

View File

@@ -0,0 +1,45 @@
import normalise from "./normalise-svg.js";
let M = { x: false, y: false };
/**
* ...
*/
function makeBezier(Bezier, term, values) {
if (term === "Z") return;
if (term === "M") {
M = { x: values[0], y: values[1] };
return;
}
const curve = new Bezier(M.x, M.y, ...values);
const last = values.slice(-2);
M = { x: last[0], y: last[1] };
return curve;
}
/**
* ...
*/
function convertPath(Bezier, d) {
const terms = normalise(d).split(" "),
matcher = new RegExp("[MLCQZ]", "");
let term,
segment,
values,
segments = [],
ARGS = { C: 6, Q: 4, L: 2, M: 2 };
while (terms.length) {
term = terms.splice(0, 1)[0];
if (matcher.test(term)) {
values = terms.splice(0, ARGS[term]).map(parseFloat);
segment = makeBezier(Bezier, term, values);
if (segment) segments.push(segment);
}
}
return new Bezier.PolyBezier(segments);
}
export { convertPath };

View File

@@ -0,0 +1,906 @@
import { Bezier } from "./bezier.js";
// math-inlining.
const { abs, cos, sin, acos, atan2, sqrt, pow } = Math;
// cube root function yielding real roots
function crt(v) {
return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3);
}
// trig constants
const pi = Math.PI,
tau = 2 * pi,
quart = pi / 2,
// float precision significant decimal
epsilon = 0.000001,
// extremas used in bbox calculation and similar algorithms
nMax = Number.MAX_SAFE_INTEGER || 9007199254740991,
nMin = Number.MIN_SAFE_INTEGER || -9007199254740991,
// a zero coordinate, which is surprisingly useful
ZERO = { x: 0, y: 0, z: 0 };
// Bezier utility functions
const utils = {
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
Tvalues: [
-0.0640568928626056260850430826247450385909,
0.0640568928626056260850430826247450385909,
-0.1911188674736163091586398207570696318404,
0.1911188674736163091586398207570696318404,
-0.3150426796961633743867932913198102407864,
0.3150426796961633743867932913198102407864,
-0.4337935076260451384870842319133497124524,
0.4337935076260451384870842319133497124524,
-0.5454214713888395356583756172183723700107,
0.5454214713888395356583756172183723700107,
-0.6480936519369755692524957869107476266696,
0.6480936519369755692524957869107476266696,
-0.7401241915785543642438281030999784255232,
0.7401241915785543642438281030999784255232,
-0.8200019859739029219539498726697452080761,
0.8200019859739029219539498726697452080761,
-0.8864155270044010342131543419821967550873,
0.8864155270044010342131543419821967550873,
-0.9382745520027327585236490017087214496548,
0.9382745520027327585236490017087214496548,
-0.9747285559713094981983919930081690617411,
0.9747285559713094981983919930081690617411,
-0.9951872199970213601799974097007368118745,
0.9951872199970213601799974097007368118745,
],
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
Cvalues: [
0.1279381953467521569740561652246953718517,
0.1279381953467521569740561652246953718517,
0.1258374563468282961213753825111836887264,
0.1258374563468282961213753825111836887264,
0.121670472927803391204463153476262425607,
0.121670472927803391204463153476262425607,
0.1155056680537256013533444839067835598622,
0.1155056680537256013533444839067835598622,
0.1074442701159656347825773424466062227946,
0.1074442701159656347825773424466062227946,
0.0976186521041138882698806644642471544279,
0.0976186521041138882698806644642471544279,
0.086190161531953275917185202983742667185,
0.086190161531953275917185202983742667185,
0.0733464814110803057340336152531165181193,
0.0733464814110803057340336152531165181193,
0.0592985849154367807463677585001085845412,
0.0592985849154367807463677585001085845412,
0.0442774388174198061686027482113382288593,
0.0442774388174198061686027482113382288593,
0.0285313886289336631813078159518782864491,
0.0285313886289336631813078159518782864491,
0.0123412297999871995468056670700372915759,
0.0123412297999871995468056670700372915759,
],
arcfn: function (t, derivativeFn) {
const d = derivativeFn(t);
let l = d.x * d.x + d.y * d.y;
if (typeof d.z !== "undefined") {
l += d.z * d.z;
}
return sqrt(l);
},
compute: function (t, points, _3d) {
// shortcuts
if (t === 0) {
return points[0];
}
const order = points.length - 1;
if (t === 1) {
return points[order];
}
const mt = 1 - t;
let p = points;
// constant?
if (order === 0) {
return points[0];
}
// linear?
if (order === 1) {
const ret = {
x: mt * p[0].x + t * p[1].x,
y: mt * p[0].y + t * p[1].y,
};
if (_3d) {
ret.z = mt * p[0].z + t * p[1].z;
}
return ret;
}
// quadratic/cubic curve?
if (order < 4) {
let mt2 = mt * mt,
t2 = t * t,
a,
b,
c,
d = 0;
if (order === 2) {
p = [p[0], p[1], p[2], ZERO];
a = mt2;
b = mt * t * 2;
c = t2;
} else if (order === 3) {
a = mt2 * mt;
b = mt2 * t * 3;
c = mt * t2 * 3;
d = t * t2;
}
const ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
};
if (_3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
}
return ret;
}
// higher order curves: use de Casteljau's computation
const dCpts = JSON.parse(JSON.stringify(points));
while (dCpts.length > 1) {
for (let i = 0; i < dCpts.length - 1; i++) {
dCpts[i] = {
x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t,
y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t,
};
if (typeof dCpts[i].z !== "undefined") {
dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t;
}
}
dCpts.splice(dCpts.length - 1, 1);
}
return dCpts[0];
},
computeWithRatios: function (t, points, ratios, _3d) {
const mt = 1 - t,
r = ratios,
p = points;
let f1 = r[0],
f2 = r[1],
f3 = r[2],
f4 = r[3],
d;
// spec for linear
f1 *= mt;
f2 *= t;
if (p.length === 2) {
d = f1 + f2;
return {
x: (f1 * p[0].x + f2 * p[1].x) / d,
y: (f1 * p[0].y + f2 * p[1].y) / d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d,
};
}
// upgrade to quadratic
f1 *= mt;
f2 *= 2 * mt;
f3 *= t * t;
if (p.length === 3) {
d = f1 + f2 + f3;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d,
};
}
// upgrade to cubic
f1 *= mt;
f2 *= 1.5 * mt;
f3 *= 3 * mt;
f4 *= t * t * t;
if (p.length === 4) {
d = f1 + f2 + f3 + f4;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x) / d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y) / d,
z: !_3d
? false
: (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d,
};
}
},
derive: function (points, _3d) {
const dpoints = [];
for (let p = points, d = p.length, c = d - 1; d > 1; d--, c--) {
const list = [];
for (let j = 0, dpt; j < c; j++) {
dpt = {
x: c * (p[j + 1].x - p[j].x),
y: c * (p[j + 1].y - p[j].y),
};
if (_3d) {
dpt.z = c * (p[j + 1].z - p[j].z);
}
list.push(dpt);
}
dpoints.push(list);
p = list;
}
return dpoints;
},
between: function (v, m, M) {
return (
(m <= v && v <= M) ||
utils.approximately(v, m) ||
utils.approximately(v, M)
);
},
approximately: function (a, b, precision) {
return abs(a - b) <= (precision || epsilon);
},
length: function (derivativeFn) {
const z = 0.5,
len = utils.Tvalues.length;
let sum = 0;
for (let i = 0, t; i < len; i++) {
t = z * utils.Tvalues[i] + z;
sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn);
}
return z * sum;
},
map: function (v, ds, de, ts, te) {
const d1 = de - ds,
d2 = te - ts,
v2 = v - ds,
r = v2 / d1;
return ts + d2 * r;
},
lerp: function (r, v1, v2) {
const ret = {
x: v1.x + r * (v2.x - v1.x),
y: v1.y + r * (v2.y - v1.y),
};
if (!!v1.z && !!v2.z) {
ret.z = v1.z + r * (v2.z - v1.z);
}
return ret;
},
pointToString: function (p) {
let s = p.x + "/" + p.y;
if (typeof p.z !== "undefined") {
s += "/" + p.z;
}
return s;
},
pointsToString: function (points) {
return "[" + points.map(utils.pointToString).join(", ") + "]";
},
copy: function (obj) {
return JSON.parse(JSON.stringify(obj));
},
angle: function (o, v1, v2) {
const dx1 = v1.x - o.x,
dy1 = v1.y - o.y,
dx2 = v2.x - o.x,
dy2 = v2.y - o.y,
cross = dx1 * dy2 - dy1 * dx2,
dot = dx1 * dx2 + dy1 * dy2;
return atan2(cross, dot);
},
// round as string, to avoid rounding errors
round: function (v, d) {
const s = "" + v;
const pos = s.indexOf(".");
return parseFloat(s.substring(0, pos + 1 + d));
},
dist: function (p1, p2) {
const dx = p1.x - p2.x,
dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
},
closest: function (LUT, point) {
let mdist = pow(2, 63),
mpos,
d;
LUT.forEach(function (p, idx) {
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
mpos = idx;
}
});
return { mdist: mdist, mpos: mpos };
},
abcratio: function (t, n) {
// see ratio(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
const bottom = pow(t, n) + pow(1 - t, n),
top = bottom - 1;
return abs(top / bottom);
},
projectionratio: function (t, n) {
// see u(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
const top = pow(1 - t, n),
bottom = pow(t, n) + top;
return top / bottom;
},
lli8: function (x1, y1, x2, y2, x3, y3, x4, y4) {
const nx =
(x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (d == 0) {
return false;
}
return { x: nx / d, y: ny / d };
},
lli4: function (p1, p2, p3, p4) {
const x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
x3 = p3.x,
y3 = p3.y,
x4 = p4.x,
y4 = p4.y;
return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4);
},
lli: function (v1, v2) {
return utils.lli4(v1, v1.c, v2, v2.c);
},
makeline: function (p1, p2) {
const x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
dx = (x2 - x1) / 3,
dy = (y2 - y1) / 3;
return new Bezier(
x1,
y1,
x1 + dx,
y1 + dy,
x1 + 2 * dx,
y1 + 2 * dy,
x2,
y2
);
},
findbbox: function (sections) {
let mx = nMax,
my = nMax,
MX = nMin,
MY = nMin;
sections.forEach(function (s) {
const bbox = s.bbox();
if (mx > bbox.x.min) mx = bbox.x.min;
if (my > bbox.y.min) my = bbox.y.min;
if (MX < bbox.x.max) MX = bbox.x.max;
if (MY < bbox.y.max) MY = bbox.y.max;
});
return {
x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx },
y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my },
};
},
shapeintersections: function (
s1,
bbox1,
s2,
bbox2,
curveIntersectionThreshold
) {
if (!utils.bboxoverlap(bbox1, bbox2)) return [];
const intersections = [];
const a1 = [s1.startcap, s1.forward, s1.back, s1.endcap];
const a2 = [s2.startcap, s2.forward, s2.back, s2.endcap];
a1.forEach(function (l1) {
if (l1.virtual) return;
a2.forEach(function (l2) {
if (l2.virtual) return;
const iss = l1.intersects(l2, curveIntersectionThreshold);
if (iss.length > 0) {
iss.c1 = l1;
iss.c2 = l2;
iss.s1 = s1;
iss.s2 = s2;
intersections.push(iss);
}
});
});
return intersections;
},
makeshape: function (forward, back, curveIntersectionThreshold) {
const bpl = back.points.length;
const fpl = forward.points.length;
const start = utils.makeline(back.points[bpl - 1], forward.points[0]);
const end = utils.makeline(forward.points[fpl - 1], back.points[0]);
const shape = {
startcap: start,
forward: forward,
back: back,
endcap: end,
bbox: utils.findbbox([start, forward, back, end]),
};
shape.intersections = function (s2) {
return utils.shapeintersections(
shape,
shape.bbox,
s2,
s2.bbox,
curveIntersectionThreshold
);
};
return shape;
},
getminmax: function (curve, d, list) {
if (!list) return { min: 0, max: 0 };
let min = nMax,
max = nMin,
t,
c;
if (list.indexOf(0) === -1) {
list = [0].concat(list);
}
if (list.indexOf(1) === -1) {
list.push(1);
}
for (let i = 0, len = list.length; i < len; i++) {
t = list[i];
c = curve.get(t);
if (c[d] < min) {
min = c[d];
}
if (c[d] > max) {
max = c[d];
}
}
return { min: min, mid: (min + max) / 2, max: max, size: max - min };
},
align: function (points, line) {
const tx = line.p1.x,
ty = line.p1.y,
a = -atan2(line.p2.y - ty, line.p2.x - tx),
d = function (v) {
return {
x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a),
y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a),
};
};
return points.map(d);
},
roots: function (points, line) {
line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } };
const order = points.length - 1;
const aligned = utils.align(points, line);
const reduce = function (t) {
return 0 <= t && t <= 1;
};
if (order === 2) {
const a = aligned[0].y,
b = aligned[1].y,
c = aligned[2].y,
d = a - 2 * b + c;
if (d !== 0) {
const m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2].filter(reduce);
} else if (b !== c && d === 0) {
return [(2 * b - c) / (2 * b - 2 * c)].filter(reduce);
}
return [];
}
// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
const pa = aligned[0].y,
pb = aligned[1].y,
pc = aligned[2].y,
pd = aligned[3].y;
let d = -pa + 3 * pb - 3 * pc + pd,
a = 3 * pa - 6 * pb + 3 * pc,
b = -3 * pa + 3 * pb,
c = pa;
if (utils.approximately(d, 0)) {
// this is not a cubic curve.
if (utils.approximately(a, 0)) {
// in fact, this is not a quadratic curve either.
if (utils.approximately(b, 0)) {
// in fact in fact, there are no solutions.
return [];
}
// linear solution:
return [-c / b].filter(reduce);
}
// quadratic solution:
const q = sqrt(b * b - 4 * a * c),
a2 = 2 * a;
return [(q - b) / a2, (-b - q) / a2].filter(reduce);
}
// at this point, we know we need a cubic solution:
a /= d;
b /= d;
c /= d;
const p = (3 * b - a * a) / 3,
p3 = p / 3,
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27,
q2 = q / 2,
discriminant = q2 * q2 + p3 * p3 * p3;
let u1, v1, x1, x2, x3;
if (discriminant < 0) {
const mp3 = -p / 3,
mp33 = mp3 * mp3 * mp3,
r = sqrt(mp33),
t = -q / (2 * r),
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
phi = acos(cosphi),
crtr = crt(r),
t1 = 2 * crtr;
x1 = t1 * cos(phi / 3) - a / 3;
x2 = t1 * cos((phi + tau) / 3) - a / 3;
x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3;
return [x1, x2, x3].filter(reduce);
} else if (discriminant === 0) {
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
x1 = 2 * u1 - a / 3;
x2 = -u1 - a / 3;
return [x1, x2].filter(reduce);
} else {
const sd = sqrt(discriminant);
u1 = crt(-q2 + sd);
v1 = crt(q2 + sd);
return [u1 - v1 - a / 3].filter(reduce);
}
},
droots: function (p) {
// quadratic roots are easy
if (p.length === 3) {
const a = p[0],
b = p[1],
c = p[2],
d = a - 2 * b + c;
if (d !== 0) {
const m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2];
} else if (b !== c && d === 0) {
return [(2 * b - c) / (2 * (b - c))];
}
return [];
}
// linear roots are even easier
if (p.length === 2) {
const a = p[0],
b = p[1];
if (a !== b) {
return [a / (a - b)];
}
return [];
}
},
curvature: function (t, points, _3d, kOnly) {
const dpoints = utils.derive(points);
const d1 = dpoints[0];
const d2 = dpoints[1];
let num,
dnm,
adk,
dk,
k = 0,
r = 0;
//
// We're using the following formula for curvature:
//
// x'y" - y'x"
// k(t) = ------------------
// (x'² + y'²)^(3/2)
//
// from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition
//
// With it corresponding 3D counterpart:
//
// sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²)
// k(t) = -------------------------------------------------------
// (x'² + y'² + z'²)^(3/2)
//
const d = utils.compute(t, d1);
const dd = utils.compute(t, d2);
const qdsum = d.x * d.x + d.y * d.y;
if (_3d) {
num = sqrt(
pow(d.y * dd.z - dd.y * d.z, 2) +
pow(d.z * dd.x - dd.z * d.x, 2) +
pow(d.x * dd.y - dd.x * d.y, 2)
);
dnm = pow(qdsum + d.z * d.z, 3 / 2);
} else {
num = d.x * dd.y - d.y * dd.x;
dnm = pow(qdsum, 3 / 2);
}
if (num === 0 || dnm === 0) {
return { k: 0, r: 0 };
}
k = num / dnm;
r = dnm / num;
// We're also computing the derivative of kappa, because
// there is value in knowing the rate of change for the
// curvature along the curve. And we're just going to
// ballpark it based on an epsilon.
if (!kOnly) {
// compute k'(t) based on the interval before, and after it,
// to at least try to not introduce forward/backward pass bias.
const pk = utils.curvature(t - 0.001, points, _3d, true).k;
const nk = utils.curvature(t + 0.001, points, _3d, true).k;
dk = (nk - k + (k - pk)) / 2;
adk = (abs(nk - k) + abs(k - pk)) / 2;
}
return { k: k, r: r, dk: dk, adk: adk };
},
inflections: function (points) {
if (points.length < 4) return [];
// FIXME: TODO: add in inflection abstraction for quartic+ curves?
const p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }),
a = p[2].x * p[1].y,
b = p[3].x * p[1].y,
c = p[1].x * p[2].y,
d = p[3].x * p[2].y,
v1 = 18 * (-3 * a + 2 * b + 3 * c - d),
v2 = 18 * (3 * a - b - 3 * c),
v3 = 18 * (c - a);
if (utils.approximately(v1, 0)) {
if (!utils.approximately(v2, 0)) {
let t = -v3 / v2;
if (0 <= t && t <= 1) return [t];
}
return [];
}
const trm = v2 * v2 - 4 * v1 * v3,
sq = Math.sqrt(trm),
d2 = 2 * v1;
if (utils.approximately(d2, 0)) return [];
return [(sq - v2) / d2, -(v2 + sq) / d2].filter(function (r) {
return 0 <= r && r <= 1;
});
},
bboxoverlap: function (b1, b2) {
const dims = ["x", "y"],
len = dims.length;
for (let i = 0, dim, l, t, d; i < len; i++) {
dim = dims[i];
l = b1[dim].mid;
t = b2[dim].mid;
d = (b1[dim].size + b2[dim].size) / 2;
if (abs(l - t) >= d) return false;
}
return true;
},
expandbox: function (bbox, _bbox) {
if (_bbox.x.min < bbox.x.min) {
bbox.x.min = _bbox.x.min;
}
if (_bbox.y.min < bbox.y.min) {
bbox.y.min = _bbox.y.min;
}
if (_bbox.z && _bbox.z.min < bbox.z.min) {
bbox.z.min = _bbox.z.min;
}
if (_bbox.x.max > bbox.x.max) {
bbox.x.max = _bbox.x.max;
}
if (_bbox.y.max > bbox.y.max) {
bbox.y.max = _bbox.y.max;
}
if (_bbox.z && _bbox.z.max > bbox.z.max) {
bbox.z.max = _bbox.z.max;
}
bbox.x.mid = (bbox.x.min + bbox.x.max) / 2;
bbox.y.mid = (bbox.y.min + bbox.y.max) / 2;
if (bbox.z) {
bbox.z.mid = (bbox.z.min + bbox.z.max) / 2;
}
bbox.x.size = bbox.x.max - bbox.x.min;
bbox.y.size = bbox.y.max - bbox.y.min;
if (bbox.z) {
bbox.z.size = bbox.z.max - bbox.z.min;
}
},
pairiteration: function (c1, c2, curveIntersectionThreshold) {
const c1b = c1.bbox(),
c2b = c2.bbox(),
r = 100000,
threshold = curveIntersectionThreshold || 0.5;
if (
c1b.x.size + c1b.y.size < threshold &&
c2b.x.size + c2b.y.size < threshold
) {
return [
(((r * (c1._t1 + c1._t2)) / 2) | 0) / r +
"/" +
(((r * (c2._t1 + c2._t2)) / 2) | 0) / r,
];
}
const cc1 = c1.split(0.5),
cc2 = c2.split(0.5),
pairs = [
{ left: cc1.left, right: cc2.left },
{ left: cc1.left, right: cc2.right },
{ left: cc1.right, right: cc2.right },
{ left: cc1.right, right: cc2.left },
];
pairs = pairs.filter(function (pair) {
return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox());
});
const results = [];
if (pairs.length === 0) return results;
pairs.forEach(function (pair) {
results = results.concat(
utils.pairiteration(pair.left, pair.right, threshold)
);
});
results = results.filter(function (v, i) {
return results.indexOf(v) === i;
});
return results;
},
getccenter: function (p1, p2, p3) {
const dx1 = p2.x - p1.x,
dy1 = p2.y - p1.y,
dx2 = p3.x - p2.x,
dy2 = p3.y - p2.y,
dx1p = dx1 * cos(quart) - dy1 * sin(quart),
dy1p = dx1 * sin(quart) + dy1 * cos(quart),
dx2p = dx2 * cos(quart) - dy2 * sin(quart),
dy2p = dx2 * sin(quart) + dy2 * cos(quart),
// chord midpoints
mx1 = (p1.x + p2.x) / 2,
my1 = (p1.y + p2.y) / 2,
mx2 = (p2.x + p3.x) / 2,
my2 = (p2.y + p3.y) / 2,
// midpoint offsets
mx1n = mx1 + dx1p,
my1n = my1 + dy1p,
mx2n = mx2 + dx2p,
my2n = my2 + dy2p,
// intersection of these lines:
arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n),
r = utils.dist(arc, p1);
// arc start/end values, over mid point:
let s = atan2(p1.y - arc.y, p1.x - arc.x),
m = atan2(p2.y - arc.y, p2.x - arc.x),
e = atan2(p3.y - arc.y, p3.x - arc.x),
_;
// determine arc direction (cw/ccw correction)
if (s < e) {
// if s<m<e, arc(s, e)
// if m<s<e, arc(e, s + tau)
// if s<e<m, arc(e, s + tau)
if (s > m || m > e) {
s += tau;
}
if (s > e) {
_ = e;
e = s;
s = _;
}
} else {
// if e<m<s, arc(e, s)
// if m<e<s, arc(s, e + tau)
// if e<s<m, arc(s, e + tau)
if (e < m && m < s) {
_ = e;
e = s;
s = _;
} else {
e += tau;
}
}
// assign and done.
arc.s = s;
arc.e = e;
arc.r = r;
return arc;
},
numberSort: function (a, b) {
return a - b;
},
};
export { utils };

View File

@@ -14,10 +14,8 @@
"url": "https://github.com/Pomax/bezierinfo/issues"
},
"scripts": {
"start": "run-s build",
"build": "node ./tools/build.js",
"deps": "run-s dep:bezier",
"dep:bezier": "cp -r ./node_modules/bezier-js/lib ./lib/bezierjs",
"start": "run-s deps build",
"test": "run-p server browser",
"server": "http-server -p 8000 --cors",
"browser": "open-cli http://localhost:8000"

View File

@@ -10,7 +10,7 @@ export default function rewriteGraphicsElement(code, width, height) {
return prettier.format(`
import CanvasBuilder from 'canvas';
import { GraphicsAPI, Bezier, Point } from "../../lib/custom-element/api/graphics-api.js";
import { GraphicsAPI, Bezier, Vector } from "../../lib/custom-element/api/graphics-api.js";
const noop = (()=>{});