diff --git a/chapters/whatis/interpolation.js b/chapters/whatis/interpolation.js index 9ef4b40d..67edc3dd 100644 --- a/chapters/whatis/interpolation.js +++ b/chapters/whatis/interpolation.js @@ -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})); } diff --git a/images/chapters/introduction/cubic.png b/images/chapters/introduction/cubic.png index a4fc51bb..ec23f943 100644 Binary files a/images/chapters/introduction/cubic.png and b/images/chapters/introduction/cubic.png differ diff --git a/images/chapters/introduction/quadratic.png b/images/chapters/introduction/quadratic.png index 8e10c4ae..ab30ca12 100644 Binary files a/images/chapters/introduction/quadratic.png and b/images/chapters/introduction/quadratic.png differ diff --git a/images/chapters/whatis/interpolation.png b/images/chapters/whatis/interpolation.png index 7267adcd..b90ef554 100644 Binary files a/images/chapters/whatis/interpolation.png and b/images/chapters/whatis/interpolation.png differ diff --git a/lib/bezierjs/bezier.js b/lib/bezierjs/bezier.js deleted file mode 100644 index 25196206..00000000 --- a/lib/bezierjs/bezier.js +++ /dev/null @@ -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 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; -})(); diff --git a/lib/bezierjs/lib/bezier.js b/lib/bezierjs/lib/bezier.js deleted file mode 100644 index 25196206..00000000 --- a/lib/bezierjs/lib/bezier.js +++ /dev/null @@ -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 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; -})(); diff --git a/lib/bezierjs/lib/normalise-svg.js b/lib/bezierjs/lib/normalise-svg.js deleted file mode 100644 index b5fd3230..00000000 --- a/lib/bezierjs/lib/normalise-svg.js +++ /dev/null @@ -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; diff --git a/lib/bezierjs/lib/poly-bezier.js b/lib/bezierjs/lib/poly-bezier.js deleted file mode 100644 index 17656960..00000000 --- a/lib/bezierjs/lib/poly-bezier.js +++ /dev/null @@ -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; -})(); diff --git a/lib/bezierjs/lib/svg-to-beziers.js b/lib/bezierjs/lib/svg-to-beziers.js deleted file mode 100644 index 3923a04f..00000000 --- a/lib/bezierjs/lib/svg-to-beziers.js +++ /dev/null @@ -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; diff --git a/lib/bezierjs/lib/utils.js b/lib/bezierjs/lib/utils.js deleted file mode 100644 index 32cd5635..00000000 --- a/lib/bezierjs/lib/utils.js +++ /dev/null @@ -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 || m > e) { - s += tau; - } - if (s > e) { - _ = e; - e = s; - s = _; - } - } else { - // if e 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 || m > e) { - s += tau; - } - if (s > e) { - _ = e; - e = s; - s = _; - } - } else { - // if e 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 }; diff --git a/lib/custom-element/api/types/bezier.js b/lib/custom-element/api/types/bezier.js index d7ed27db..fbae1100 100644 --- a/lib/custom-element/api/types/bezier.js +++ b/lib/custom-element/api/types/bezier.js @@ -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(); } } diff --git a/lib/custom-element/api/types/bezier/base.js b/lib/custom-element/api/types/bezier/base.js deleted file mode 100644 index a7c4f188..00000000 --- a/lib/custom-element/api/types/bezier/base.js +++ /dev/null @@ -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 { - 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 } diff --git a/lib/custom-element/api/types/point.js b/lib/custom-element/api/types/point.js deleted file mode 100644 index 605b4ec4..00000000 --- a/lib/custom-element/api/types/point.js +++ /dev/null @@ -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 }; diff --git a/lib/custom-element/api/types/vector.js b/lib/custom-element/api/types/vector.js new file mode 100644 index 00000000..591dbfcd --- /dev/null +++ b/lib/custom-element/api/types/vector.js @@ -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 }; diff --git a/lib/custom-element/graphics-element.js b/lib/custom-element/graphics-element.js index e70cb464..4dbe1f5e 100644 --- a/lib/custom-element/graphics-element.js +++ b/lib/custom-element/graphics-element.js @@ -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} diff --git a/lib/custom-element/lib/bezierjs/bezier.js b/lib/custom-element/lib/bezierjs/bezier.js new file mode 100644 index 00000000..2adcbacd --- /dev/null +++ b/lib/custom-element/lib/bezierjs/bezier.js @@ -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 }; diff --git a/lib/bezierjs/normalise-svg.js b/lib/custom-element/lib/bezierjs/normalise-svg.js similarity index 67% rename from lib/bezierjs/normalise-svg.js rename to lib/custom-element/lib/bezierjs/normalise-svg.js index b5fd3230..9a9f793a 100644 --- a/lib/bezierjs/normalise-svg.js +++ b/lib/custom-element/lib/bezierjs/normalise-svg.js @@ -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; diff --git a/lib/custom-element/lib/bezierjs/poly-bezier.js b/lib/custom-element/lib/bezierjs/poly-bezier.js new file mode 100644 index 00000000..091acc80 --- /dev/null +++ b/lib/custom-element/lib/bezierjs/poly-bezier.js @@ -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 }; diff --git a/lib/custom-element/lib/bezierjs/svg-to-beziers.js b/lib/custom-element/lib/bezierjs/svg-to-beziers.js new file mode 100644 index 00000000..9a49a91f --- /dev/null +++ b/lib/custom-element/lib/bezierjs/svg-to-beziers.js @@ -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 }; diff --git a/lib/custom-element/lib/bezierjs/utils.js b/lib/custom-element/lib/bezierjs/utils.js new file mode 100644 index 00000000..3a55db4c --- /dev/null +++ b/lib/custom-element/lib/bezierjs/utils.js @@ -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 || m > e) { + s += tau; + } + if (s > e) { + _ = e; + e = s; + s = _; + } + } else { + // if e{});