commit 4ced372eff0afcfc7d8c6d07720df3d9d0ca99f3 Author: Caleb Braaten Date: Wed Apr 3 18:04:39 2024 -0700 First Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d169f6a --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# HUE LightShow + +## Motivation + +For the gender reveal of my second kid, I wanted to do something unique and fun. It was the perfect excuse to justify the purchase of a Philips Hue Lightstrip to the Mrs. + +I wrapped the light strip around our Christmas tree and wrote some Javascipt to alternate between blue and pink. There were a couple iterations + +## Technical Details + +`lib.js` contains some helper functions (and authentication to the Hue Bridge) that leverage the Philips Hue REST API. These functions are used in `main.js` to control the light strip. + +The first iteration of the light show was a simple alternating sequence that loaded all the color transitions into the event loop at the start with the setTimeout progressively having a longer delay to create the sequence. + +The linear nature was a bit boring, so I pulled in a [bezier.js library](https://github.com/Pomax/bezierjs/blob/master/src/utils.js) (Thank you [Pomax](https://github.com/Pomax)) to create a more dynamic pacing of the sequence. + +Following the same setTimeout strategy, I created a list of values from the bezier curve to use as the root of the setTimeout delay making the sequence speed up and then slow down to a stop. Once the sequence was complete, the final color would start to glow in and out. + +## Video + +Here is the final product. Please be aware, the video contains rapid flashes near the beginning. If you are sensitive to flashing lights and still want to watch it, consider skipping to the middle of the video where the sequence slows down considerably. + +[![HUE Light Show](https://img.youtube.com/vi/1Q2J9zv3Q1Y/0.jpg)](https://photos.app.goo.gl/Qst3tRiBQrFXAEuZ6) diff --git a/bezier.js b/bezier.js new file mode 100644 index 0000000..25008dc --- /dev/null +++ b/bezier.js @@ -0,0 +1,1973 @@ +// 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 !== undefined && v2.z !== undefined) { + 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) { + return new Bezier( + p1.x, + p1.y, + (p1.x + p2.x) / 2, + (p1.y + p2.y) / 2, + p2.x, + p2.y + ); + }, + + 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 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; + + // is this curve, practically speaking, a straight line? + const aligned = utils.align(points, { p1: points[0], p2: points[order] }); + const baselength = utils.dist(points[0], points[order]); + this._linear = aligned.reduce((t, p) => t + abs$1(p.y), 0) < baselength / 50; + + this._lut = []; + + this._t1 = 0; + this._t2 = 1; + this.update(); + } + + 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 = Bezier.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 = Bezier.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)); + } + + static getABC(order = 2, S, B, E, t = 0.5) { + const u = utils.projectionratio(t, order), + um = 1 - u, + C = { + x: u * S.x + um * E.x, + y: u * S.y + um * E.y, + }, + s = utils.abcratio(t, order), + A = { + x: B.x + (B.x - C.x) / s, + y: B.y + (B.y - C.y) / s, + }; + return { A, B, C, S, E }; + } + + getABC(t, B) { + B = B || this.get(t); + let S = this.points[0]; + let E = this.points[this.order]; + return Bezier.getABC(this.order, S, B, E, t); + } + + getLUT(steps) { + this.verify(); + steps = steps || 100; + if (this._lut.length === steps) { + return this._lut; + } + this._lut = []; + // n steps means n+1 points + steps++; + this._lut = []; + for (let i = 0, p, t; i < steps; i++) { + t = i / (steps - 1); + p = this.compute(t); + p.t = t; + this._lut.push(p); + } + 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; + } + } + ft = ft < 0 ? 0 : ft > 1 ? 1 : ft; + 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) { + return utils.compute(t, this.dpoints[0], this._3d); + } + + dderivative(t) { + return utils.compute(t, this.dpoints[1], this._3d); + } + + align() { + let p = this.points; + return new Bezier(utils.align(p, { p1: p[0], p2: p[p.length - 1] })); + } + + curvature(t) { + return utils.curvature(t, this.dpoints[0], this.dpoints[1], 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$1(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$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; + 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$1(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$1(acos$1(s)) < pi$1 / 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$1(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; + } + + translate(v, d1, d2) { + d2 = typeof d2 === "number" ? d2 : d1; + + // TODO: make this take curves with control points outside + // of the start-end interval into account + + const o = this.order; + let d = this.points.map((_, i) => (1 - i / o) * d1 + (i / o) * d2); + return new Bezier( + this.points.map((p, i) => ({ + x: p.x + v.x * d[i], + y: p.y + v.y * d[i], + })) + ); + } + + 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 non-linear degenerate curves. + + const clockwise = this.clockwise; + const points = this.points; + + if (this._linear) { + return this.translate( + this.normal(0), + distanceFn ? distanceFn(0) : d, + distanceFn ? distanceFn(1) : d + ); + } + + const r1 = distanceFn ? distanceFn(0) : d; + const r2 = distanceFn ? distanceFn(1) : d; + const v = [this.offset(0, 10), this.offset(1, 10)]; + 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', + // and 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$1(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 = d2 === undefined ? d1 : d2; + + if (this._linear) { + // TODO: find the actual extrema, because they might + // be before the start, or past the end. + + const n = this.normal(0); + const start = this.points[0]; + const end = this.points[this.points.length - 1]; + let s, mid, e; + + if (d3 === undefined) { + d3 = d1; + d4 = d2; + } + + s = { x: start.x + n.x * d1, y: start.y + n.y * d1 }; + e = { x: end.x + n.x * d3, y: end.y + n.y * d3 }; + mid = { x: (s.x + e.x) / 2, y: (s.y + e.y) / 2 }; + const fline = [s, mid, e]; + + s = { x: start.x - n.x * d2, y: start.y - n.y * d2 }; + e = { x: end.x - n.x * d4, y: end.y - n.y * d4 }; + mid = { x: (s.x + e.x) / 2, y: (s.y + e.y) / 2 }; + const bline = [e, mid, s]; + + const ls = utils.makeline(bline[2], fline[0]); + const le = utils.makeline(fline[2], bline[0]); + const segments = [ls, new Bezier(fline), le, new Bezier(bline)]; + return new PolyBezier(segments); + } + + const reduced = this.reduce(), + len = reduced.length, + fcurves = []; + + let bcurves = [], + 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) { + 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)) + ); + } 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); + + 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.push(...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$1(d1 - ref) + abs$1(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 2: find the best possible arc + do { + prev_good = curr_good; + prev_arc = arc; + t_m = (t_s + t_e) / 2; + + 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$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)); + } + 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; \ No newline at end of file diff --git a/lib.js b/lib.js new file mode 100644 index 0000000..831f15e --- /dev/null +++ b/lib.js @@ -0,0 +1,91 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +const baseURL = 'https://192.168.254.49/clip/v2/resource/' +const resource = 'light/c84fd63f-aa24-4203-9d92-dfa15e65bd54' + +const headers = { + 'Content-Type': 'application/json', + 'hue-application-key': 'NpvdpFNOk3ROMGeWi2R598Jz31zyZ8Eiv5HC3S2o' +}; + +function setBlue(delay){ + const body = `{ + "color": { + "xy": { + "x":0.16, + "y":0.08 + } + }, + "dynamics": { + "duration": 0 + } + }` + console.log("Send Blue @", delay) + sendRequest(body) +} + +function setPink(delay){ + const body = `{ + "color": { + "xy": { + "x":0.45, + "y":0.20 + } + }, + "dynamics": { + "duration": 0 + } + }` + + console.log("Send Pink @", delay) + sendRequest(body) +} + +function sendRequest(body){ + try { + fetch(`${baseURL}${resource}`, { + method: 'PUT', + headers, + body + }) + .then( + (res) => { + // console.log("Status:", res.status) + return res.json() + } + ) + .then( + // (res) => console.log("Body:", res) + ) + } catch (error) { + console.log(error) + } +} + +function glow(delay){ + const body = `{ + "dimming": { + "brightness":100.0 + }, + "dynamics": { + "duration": 1000 + } + }` + + const body2 = `{ + "dimming": { + "brightness":30.0 + }, + "dynamics": { + "duration": 1000 + } + }` + + console.log("Send Glow (up and down) @", delay) + setTimeout(sendRequest, 1000, body) + setTimeout(sendRequest, 2000, body2) +} + + + + +module.exports = { setBlue, setPink, glow } diff --git a/main.js b/main.js new file mode 100644 index 0000000..b48152b --- /dev/null +++ b/main.js @@ -0,0 +1,65 @@ +const Bezier = require("./bezier"); +const hue = require("./lib"); + +// hue.setBlue() +// hue.setPink() + +// Linear Rendering (Working) +// for(let i = 0; i < 101; i++){ +// if(i % 2 === 0){ +// setTimeout(hue.setBlue, i*100) +// } +// else{ +// setTimeout(hue.setPink, i*100) +// } +// } +// console.warn("Done Scheduling Events") + +// If bezzier doesn't work, go back to lame segmentation +// for(let i = 0; i < 101; i++){ +// if(i % 2 === 0){ +// if(i < 10){ +// setTimeout(hue.setBlue, i*100) +// } +// else if (10 < i < 20){ + +// } +// else if (20 < ) +// } +// else{ +// setTimeout(hue.setPink, i*100) +// } +// } +// console.warn("Done Scheduling Events") + +// Linear Rendering (Working) +const b = new Bezier(0, 400, 1000, 100, 100, 100, 1000, 400); +let points = b.getLUT(); + +// + (50 * points[i].x) + +for (let i = 0; i < points.length; i++) { + console.log(i * points[i].y); +} + +for (let i = 0; i < 100; i++) { + var delay = i * points[i].y; + if (i % 2 === 0) { + setTimeout(hue.setBlue, delay, delay); + } else { + setTimeout(hue.setPink, delay, delay); + } +} + +setTimeout( + () => { + hue.glow(); + for (let i = 1; i < 100; i++) { + setTimeout(hue.glow, i * 2000, i * 2000); + } + }, + delay, + delay, +); + +console.warn("Done Scheduling Events");