diff --git a/docs/chapters/control/lerp.js b/docs/chapters/control/lerp.js index 553b110e..30f4f93a 100644 --- a/docs/chapters/control/lerp.js +++ b/docs/chapters/control/lerp.js @@ -33,7 +33,7 @@ setup() { // and capturing the resulting Shape object that yields. We'll draw // those in the draw() function. this.shapes = this.interpolationFunctions.map(f => plot(f, 0, 1, degree*5) ); - + noGrid(); setSlider(`.slide-control`, `position`, 0) } diff --git a/docs/images/chapters/control/1b8c5e574dc67bfb0afc3fb0a8727378.png b/docs/images/chapters/control/1b8c5e574dc67bfb0afc3fb0a8727378.png new file mode 100644 index 00000000..39aaa2f1 Binary files /dev/null and b/docs/images/chapters/control/1b8c5e574dc67bfb0afc3fb0a8727378.png differ diff --git a/docs/images/chapters/control/8332e5d34b7344bbee2a2e1f4521ce46.png b/docs/images/chapters/control/8332e5d34b7344bbee2a2e1f4521ce46.png new file mode 100644 index 00000000..29e2128f Binary files /dev/null and b/docs/images/chapters/control/8332e5d34b7344bbee2a2e1f4521ce46.png differ diff --git a/docs/images/chapters/control/c26d2655e8741ef7e2eeb4f6554fc7a5.png b/docs/images/chapters/control/c26d2655e8741ef7e2eeb4f6554fc7a5.png new file mode 100644 index 00000000..9894b8c7 Binary files /dev/null and b/docs/images/chapters/control/c26d2655e8741ef7e2eeb4f6554fc7a5.png differ diff --git a/docs/index.html b/docs/index.html index 9c0de75e..68e6ac6f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -38,7 +38,7 @@ - + @@ -1008,7 +1008,7 @@ function Bezier(3,t): Scripts are disabled. Showing fallback image. - + @@ -1016,7 +1016,7 @@ function Bezier(3,t): Scripts are disabled. Showing fallback image. - + @@ -1024,7 +1024,7 @@ function Bezier(3,t): Scripts are disabled. Showing fallback image. - + diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 5853d12f..4feda82c 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -41,7 +41,7 @@ - + @@ -974,7 +974,7 @@ function Bezier(3,t): > Scripts are disabled. Showing fallback image. - + @@ -990,7 +990,7 @@ function Bezier(3,t): > Scripts are disabled. Showing fallback image. - + @@ -1006,7 +1006,7 @@ function Bezier(3,t): > Scripts are disabled. Showing fallback image. - + diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 373d5146..48b4c1c0 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -12,6 +12,7 @@ const MOUSE_PRECISION_ZONE = 5; const TOUCH_PRECISION_ZONE = 30; let CURRENT_HUE = 0; +let CURRENT_CURSOR = `pointer`; /** * Our Graphics API, which is the "public" side of the API. @@ -276,11 +277,34 @@ class GraphicsAPI extends BaseAPI { } } + /** + * Set a (CSS) margin around the canvas + */ + setMargin(width = 0) { + this.canvas.style.marginTop = `${width}px`; + this.canvas.style.marginBottom = `${width}px`; + } + + /** + * Hide the cursor in a way that we can restore later. + */ + hideCursor() { + this.canvas.style.cursor = `none`; + } + + /** + * Rebind the cursor to what it should be. + */ + showCursor() { + this.canvas.style.cursor = CURRENT_CURSOR; + } + /** * Set the cursor type while the cursor is over the canvas */ setCursor(type) { - this.canvas.style.cursor = type; + CURRENT_CURSOR = type; + this.showCursor(); } /** diff --git a/docs/js/custom-element/api/impart-slider-logic.js b/docs/js/custom-element/api/impart-slider-logic.js index b026b94f..f9d902e7 100644 --- a/docs/js/custom-element/api/impart-slider-logic.js +++ b/docs/js/custom-element/api/impart-slider-logic.js @@ -14,7 +14,7 @@ export default function impartSliderLogic(GraphicsAPI) { slider.setAttribute(`value`, value); slider.setAttribute(`class`, classes); this.element.append(slider); - this.setSlider(slider, propname, value, transform); + return this.setSlider(slider, propname, value, transform); } }; diff --git a/docs/js/custom-element/graphics-element.js b/docs/js/custom-element/graphics-element.js index 8ad5758f..c11db1fc 100644 --- a/docs/js/custom-element/graphics-element.js +++ b/docs/js/custom-element/graphics-element.js @@ -78,7 +78,7 @@ class GraphicsElement extends CustomElement { :host([hidden]) { display: none; } :host { max-width: calc(2em + ${this.getAttribute(`width`)}px); } :host style { display: none; } - :host .top-title { display: flex; flex-direction: row; justify-content: space-between; } + :host .top-title { display: flex; flex-direction: row-reverse; justify-content: space-between; } :host canvas { position: relative; z-index: 1; display: block; margin: auto; border-radius: 0; box-sizing: content-box!important; border: 1px solid lightgrey; } :host canvas:focus { border: 1px solid red; } :host a.view-source { font-size: 60%; text-decoration: none; } @@ -204,6 +204,8 @@ class GraphicsElement extends CustomElement { return `${main} "${modulebase}${group}"`; }); + this.linkableCode = code; + // Then, step 2: split up the code into "global" vs. "class" code. const split = splitCodeSections(code); const globalCode = split.quasiGlobal; @@ -328,19 +330,21 @@ class GraphicsElement extends CustomElement { const toptitle = document.createElement(`div`); toptitle.classList.add(`top-title`); - const a = document.createElement(`a`); - a.classList.add(`view-source`); - a.textContent = this.getAttribute(`viewSource`) || `view source`; - a.href = this.src; - a.target = `_blank`; - toptitle.append(a); - const r = document.createElement(`button`); r.classList.add(`reset`); r.textContent = this.getAttribute(`reset`) || `reset`; r.addEventListener(`click`, () => this.reset()); toptitle.append(r); + if (this.src) { + const a = document.createElement(`a`); + a.classList.add(`view-source`); + a.textContent = this.getAttribute(`viewSource`) || `view source`; + a.href = this.src; + a.target = `_blank`; + toptitle.append(a); + } + if (this.label) slotParent.insertBefore(toptitle, this.canvas); } } diff --git a/docs/js/custom-element/lib/bezierjs/bezier.js b/docs/js/custom-element/lib/bezierjs/bezier.js index 8d7d3f4f..1bf1e3c3 100644 --- a/docs/js/custom-element/lib/bezierjs/bezier.js +++ b/docs/js/custom-element/lib/bezierjs/bezier.js @@ -1,3 +1,1208 @@ +// 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) { + points[0].t = 0; + return points[0]; + } + + const order = points.length - 1; + + if (t === 1) { + points[order].t = 1; + return points[order]; + } + + const mt = 1 - t; + let p = points; + + // constant? + if (order === 0) { + points[0].t = t; + 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, + t: t, + }; + 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, + t: t, + }; + 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); + } + dCpts[0].t = t; + 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, + t: t, + }; + } + + // 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, + t: t, + }; + } + + // 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, + t: t, + }; + } + }, + + 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 []; + } + + return []; + }, + + curvature: function (t, d1, d2, _3d, kOnly) { + 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, d1, d2, _3d, true).k; + const nk = utils.curvature(t + 0.001, d1, d2, _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]; + } + + let 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()); + }); + + let 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 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 + " "; + } + } + } + + // lineto commands + else if (lop === "l") { + 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 + " "; + } + } else if (lop === "h") { + for (a = 0; a < alen; a++) { + if (op === "h") { + x += args[a]; + } else { + x = args[a]; + } + normalized += "L " + x + " " + y + " "; + } + } else if (lop === "v") { + for (a = 0; a < alen; a++) { + if (op === "v") { + y += args[a]; + } else { + y = args[a]; + } + normalized += "L " + x + " " + y + " "; + } + } + + // quadratic curveto commands + else if (lop === "q") { + 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 + " "; + } + } 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 + " "; + } + } + + // cubic curveto commands + else if (lop === "c") { + 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 + " "; + } + } 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 + " "; + } + } + + // 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 "; + // not unimportant: path closing changes the current x/y coordinate + x = sx; + y = sy; + } + } + return normalized.trim(); +} + +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 = normalizePath(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); +} + /** A javascript Bezier curve library by Pomax. @@ -6,15 +1211,9 @@ 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 }; +const { abs: abs$1, min, max, cos: cos$1, sin: sin$1, acos: acos$1, sqrt: sqrt$1 } = Math; +const pi$1 = Math.PI; /** * Bezier curve constructor. @@ -77,7 +1276,7 @@ class Bezier { 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._linear = !aligned.some((p) => abs$1(p.y) > 0.0001); this._lut = []; @@ -292,6 +1491,7 @@ class Bezier { ft = t; } } + ft = ft < 0 ? 0 : ft > 1 ? 1 : ft; p = this.compute(ft); p.t = ft; p.d = mdist; @@ -356,7 +1556,7 @@ class Bezier { __normal2(t) { const d = this.derivative(t); - const q = sqrt(d.x * d.x + d.y * d.y); + const q = sqrt$1(d.x * d.x + d.y * d.y); return { x: -d.y / q, y: d.x / q }; } @@ -364,8 +1564,8 @@ class Bezier { // 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); + q1 = sqrt$1(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z), + q2 = sqrt$1(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z); r1.x /= q1; r1.y /= q1; r1.z /= q1; @@ -378,7 +1578,7 @@ class Bezier { 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); + const m = sqrt$1(c.x * c.x + c.y * c.y + c.z * c.z); c.x /= m; c.y /= m; c.z /= m; @@ -545,7 +1745,7 @@ class Bezier { if (this._3d) { s += n1.z * n2.z; } - return abs(acos(s)) < pi / 3; + return abs$1(acos$1(s)) < pi$1 / 3; } reduce() { @@ -584,7 +1784,7 @@ class Bezier { segment = p1.split(t1, t2); if (!segment.simple()) { t2 -= step; - if (abs(t1 - t2) < step) { + if (abs$1(t1 - t2) < step) { // we can never form a reduction return []; } @@ -662,7 +1862,7 @@ class Bezier { }; var rc = distanceFn ? distanceFn((t + 1) / order) : d; if (distanceFn && !clockwise) rc = -rc; - var m = sqrt(ov.x * ov.x + ov.y * ov.y); + var m = sqrt$1(ov.x * ov.x + ov.y * ov.y); ov.x /= m; ov.y /= m; np[t + 1] = { @@ -697,7 +1897,7 @@ class Bezier { // form curve oulines reduced.forEach(function (segment) { - slen = segment.length(); + const 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))); @@ -728,8 +1928,7 @@ class Bezier { 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; + segments = [ls].concat(fcurves).concat([le]).concat(bcurves); return new PolyBezier(segments); } @@ -782,7 +1981,7 @@ class Bezier { left = reduced.slice(i, i + 1); right = reduced.slice(i + 2); result = this.curveintersects(left, right, curveIntersectionThreshold); - results = results.concat(result); + results.push(...result); } return results; } @@ -820,7 +2019,7 @@ class Bezier { ref = utils.dist(pc, np1), d1 = utils.dist(pc, c1), d2 = utils.dist(pc, c2); - return abs(d1 - ref) + abs(d2 - ref); + return abs$1(d1 - ref) + abs$1(d2 - ref); } _iterate(errorThreshold, circles) { @@ -848,15 +2047,13 @@ class Bezier { // numbers: let t_m = t_e, - prev_e = 1, - step = 0; + prev_e = 1; // 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); @@ -886,8 +2083,8 @@ class Bezier { // 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), + x: arc.x + arc.r * cos$1(arc.e), + y: arc.y + arc.r * sin$1(arc.e), }; arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1)); } diff --git a/docs/news/2020-09-18.html b/docs/news/2020-09-18.html index 7095f9a6..f03a136a 100644 --- a/docs/news/2020-09-18.html +++ b/docs/news/2020-09-18.html @@ -34,7 +34,7 @@ - + diff --git a/docs/news/index.html b/docs/news/index.html index 6a05df53..9d99f65e 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -33,7 +33,7 @@ - + diff --git a/docs/news/rss.xml b/docs/news/rss.xml index 5691b203..cd832592 100644 --- a/docs/news/rss.xml +++ b/docs/news/rss.xml @@ -6,7 +6,7 @@ News updates for the primer on Bézier Curves by Pomax en-GB - Fri Oct 30 2020 20:22:30 +00:00 + Sun Nov 01 2020 14:42:49 +00:00 https://pomax.github.io/bezierinfo/images/og-image.png A Primer on Bézier Curves diff --git a/docs/uk-UA/index.html b/docs/uk-UA/index.html index 1206eb9c..4f6e905c 100644 --- a/docs/uk-UA/index.html +++ b/docs/uk-UA/index.html @@ -39,7 +39,7 @@ - + @@ -1039,7 +1039,7 @@ function Bezier(3,t): > Скрипти вимкнено. показує резервний. - + @@ -1055,7 +1055,7 @@ function Bezier(3,t): > Скрипти вимкнено. показує резервний. - + @@ -1071,7 +1071,7 @@ function Bezier(3,t): > Скрипти вимкнено. показує резервний. - + diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index c4e91b36..431be9ea 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -41,7 +41,7 @@ - + @@ -946,7 +946,7 @@ function Bezier(3,t): > Scripts are disabled. Showing fallback image. - + @@ -962,7 +962,7 @@ function Bezier(3,t): > Scripts are disabled. Showing fallback image. - + @@ -978,7 +978,7 @@ function Bezier(3,t): > Scripts are disabled. Showing fallback image. - +