diff --git a/docs/chapters/pointcurves/circle.js b/docs/chapters/pointcurves/circle.js new file mode 100644 index 00000000..21e00b4c --- /dev/null +++ b/docs/chapters/pointcurves/circle.js @@ -0,0 +1,158 @@ +let points = [], utils = Bezier.getUtils(); + +setup() { + let w = this.width/4; + let h = this.height/3; + points.push({x: 0.30 * this.width, y: 0.25 * this.height}); + points.push({x: 0.80 * this.width, y: 0.50 * this.height}); + points.push({x: 0.30 * this.width, y: 0.75 * this.height}); + if (this.parameters.showCurve) { + points[1].x = 0.66 * this.width; + points[1].y = 0.75 * this.height; + } + setMovable(points); +} + +draw() { + clear(); + + if (points.length === 3) { + let [p1, p2, p3] = points; + let c = this.caclulateCenter(p1, p2, p3); + + if (c) { + if (this.parameters.showCurve) { + this.showCurve(p1, p2, p3, c); + } else { + this.showChords(p1, p2, p3, c); + } + + setColor(`black`); + circle(c.x, c.y, 2); + text(`center`, c.x+5, c.y+5); + + setFill(`#FF000030`) + setStroke(`red`); + circle(c.x, c.y, c.r); + noFill(); + setStroke(`black`) + arc(c.x, c.y, c.r, c.s, c.e); + } + } + + setColor(`black`); + let lbl = this.parameters.showCurve ? [`start`, `B`, `end`] : [`p1`, `p2`, `p3`]; + points.forEach((p,i) => { + circle(p.x, p.y, 3); + text(lbl[i], p.x+7, p.y+7); + }); +} + +caclulateCenter(p1, p2, p3) { + // deltas + const dx1 = (p2.x - p1.x), + dy1 = (p2.y - p1.y), + dx2 = (p3.x - p2.x), + dy2 = (p3.y - p2.y); + + // perpendiculars (quarter circle turned) + const dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2), + dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2), + dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2), + dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2); + + // chord midpoints + const 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 + const mx1n = mx1 + dx1p, + my1n = my1 + dy1p, + mx2n = mx2 + dx2p, + my2n = my2 + dy2p; + + // intersection of these lines: + const i = utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n); + + if (!i) return false; + + const r = utils.dist(i,p1); + + // determine arc start/mid/end values, and direction (cw/ccw correction) + let s = atan2(p1.y - i.y, p1.x - i.x), + m = atan2(p2.y - i.y, p2.x - i.x), + e = atan2(p3.y - i.y, p3.x - i.x), + __; + + if (sm || m>e) { s += TAU; } + if (s>e) { __=e; e=s; s=__; } + } else { + if (e PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3, + de1 = t * bc, + de2 = (1-t) * bc; + + // We then determine the circle-aligned slope as normalized dx/dy + const tangent = [ + { x: B.x - 10 * (B.y-c.y), y: B.y + 10 * (B.x-c.x) }, + { x: B.x + 10 * (B.y-c.y), y: B.y - 10 * (B.x-c.x) } + ], + tlength = dist(tangent[0].x, tangent[0].y, tangent[1].x, tangent[1].y), + dx = (tangent[1].x - tangent[0].x)/tlength, + dy = (tangent[1].y - tangent[0].y)/tlength; + + line(tangent[0].x, tangent[0].y, tangent[1].x, tangent[1].y); + + // and then construct e1 and e2 as per the chapter text + const e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy}; + circle(e1.x, e1.y, 3); + text(`e1`, e1.x+15, e1.y+10); + + const e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy }; + circle(e2.x, e2.y, 3); + text(`e2`, e2.x+15, e2.y+10); +} diff --git a/docs/chapters/pointcurves/content.en-GB.md b/docs/chapters/pointcurves/content.en-GB.md index df94b2e7..6698934c 100644 --- a/docs/chapters/pointcurves/content.en-GB.md +++ b/docs/chapters/pointcurves/content.en-GB.md @@ -1,13 +1,58 @@ # Creating a curve from three points -Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a *t* value, so we can figure out where point 'C' needs to be. +Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although -The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped *t* to simply be 0.5, lets us bypass some maths, since a *t* value of 0.5 always puts C in the middle of the start--end line segment) +For quadratic curves, things are pretty easy: technically we need a `t` value in order to compute the ratio function used in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and `B` point, and `B` and end point as a ratio, using - +\[ + \left \{ \begin{aligned} + d_1 &= ||\textit{Start} - B||\\ + d_2 &= ||\textit{End} - B||\\ + t &= \frac{d_1}{d_1+d_2} + \end{aligned} \right . +\] -For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped *t* to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point: +With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using `A` as our curve's control point: - + -In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which *t* value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of *t* and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal. +For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a [chord](https://en.wikipedia.org/wiki/Chord_%28geometry%29), and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle. + +That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points: + + + +With that covered, we now also know the tangent line to our point `B`, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points `e1` and `e2` on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable `t` value, and then our `e1` and `e2` coordinates must obey the standard de Casteljau rule for linear interpolation: + +\[ + \left \{ \begin{aligned} + e_1 &= B + t \cdot d\\ + e_2 &= B - (1-t) \cdot d + \end{aligned} \right . +\] + +Where `d` is the total length of the line segment from `e1` to `e2`. So how long do we make that? There are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the length of the baseline". This forces `e1` and `e2` to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually _look_ like a line. Nice! The last thing we'll need to do is make sure to flip the sign of `d` depending on which side of the baseline our `B` is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the [atan2](https://en.wikipedia.org/wiki/Atan2) function: + +\[ + \phi = atan2(E_y-S_y, E_x-S_x) - atan2(B_y-S_y, B_x-S_x) +\] + +This angle φ will be between 0 and π if `B` is "above" the baseline (rotating all three points so that the start is on the left and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value `d`: + +\[ + d = \left \{ \begin{aligned} + d & \textit{ if } 0 \leq \phi \leq \pi \\ + -d & \textit{ if } \phi < 0 \lor \phi > \pi + \end{aligned} \right . +\] + + +The result of this approach looks as follows: + + + +It is important to remember that even though we're using a circular arc to come up with decent `e1` and `e2` terms, we're _not_ trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; [more on that later](#arcapproximation)), we're _only_ trying to come up with some reasonable `e1` and `e2` points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us: + + + +That looks perfectly servicable! diff --git a/docs/chapters/pointcurves/cubic.js b/docs/chapters/pointcurves/cubic.js new file mode 100644 index 00000000..49e5e723 --- /dev/null +++ b/docs/chapters/pointcurves/cubic.js @@ -0,0 +1,96 @@ +let points = [], utils = Bezier.getUtils(); + +setup() { + points = [ + {x: 0.5 * this.width, y: 0.25 * this.height}, + {x: 0.75 * this.width, y: 0.5 * this.height}, + {x: 0.5 * this.width, y: 0.75 * this.height}, + ]; + setMovable(points); +} + +draw() { + clear(); + let [S, B, E] = points, t; + + if (points.length === 3) { + let d1 = dist(S.x, S.y, B.x, B.y); + let d2 = dist(E.x, E.y, B.x, B.y); + t = d1 / (d1 + d2); + + let {A, C} = Bezier.getABC(3, ...points, t); + let { e1, e2, C1, C2 } = this.findControlPoints(t, A, B, C, S, E); + + circle(e1.x, e1.y, 2); + circle(e2.x, e2.y, 2); + + let curve = new Bezier(this, [S, C1, C2, E]); + + curve.drawSkeleton(`lightblue`); + curve.drawCurve(`grey`); + + setColor(`lightblue`); + circle(A.x, A.y, 2); + circle(C.x, C.y, 2); + text(`A`, A.x+5, A.y+5); + line(S.x,S.y,E.x,E.y); + line(C.x,C.y,A.x,A.y); + text(`C`, C.x+5, C.y+15); + } + + setColor(`black`); + let lbl = [`Start`, `B`, `End`]; + points.forEach((p,i) => { + circle(p.x, p.y, 3); + text(lbl[i], p.x+5, p.y+5); + }); + + if (points.length === 3) { + text(` with t=${t.toFixed(2)}`, B.x + 10, B.y+20); + } +} + +findControlPoints(t, A, B, C, S, E) { + // get our e1-e2 distances + const angle = atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x), + bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3, + de1 = t * bc, + de2 = (1-t) * bc; + + // get the circle-aligned slope as normalized dx/dy + const c = utils.getccenter(S,B,E), + tangent = [ + { x: B.x - (B.y - c.y), y: B.y + (B.x - c.x) }, + { x: B.x + (B.y - c.y), y: B.y - (B.x - c.x) } + ], + tlength = dist(tangent[0].x, tangent[0].y, tangent[1].x, tangent[1].y), + dx = (tangent[1].x - tangent[0].x)/tlength, + dy = (tangent[1].y - tangent[0].y)/tlength; + + // then set up an e1 and e2 parallel to the baseline + const e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy}; + const e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy }; + + // then use those e1/e2 to derive the new hull coordinates + const v1 = { + x: A.x + (e1.x - A.x) / (1 - t), + y: A.y + (e1.y - A.y) / (1 - t) + }; + + const v2 = { + x: A.x + (e2.x - A.x) / t, + y: A.y + (e2.y - A.y) / t + }; + + const C1 = { + x: S.x + (v1.x - S.x) / t, + y: S.y + (v1.y - S.y) / t + }; + + const C2 = { + x: E.x + (v2.x - E.x) / (1 - t), + y: E.y + (v2.y - E.y) / (1 - t), + }; + + return { e1, e2, C1, C2 }; +} \ No newline at end of file diff --git a/docs/chapters/pointcurves/handler.js b/docs/chapters/pointcurves/handler.js deleted file mode 100644 index bd562f6d..00000000 --- a/docs/chapters/pointcurves/handler.js +++ /dev/null @@ -1,164 +0,0 @@ -var abs = Math.abs; - -module.exports = { - setup: function(api) { - api.lpts = [ - {x:56, y:153}, - {x:144,y:83}, - {x:188,y:185} - ]; - }, - - onClick: function(evt, api) { - if (api.lpts.length==3) { api.lpts = []; } - api.lpts.push({ - x: evt.offsetX, - y: evt.offsetY - }); - api.redraw(); - }, - - getQRatio: function(t) { - var t2 = 2*t, - top = t2*t - t2, - bottom = top + 1; - return abs(top/bottom); - }, - - getCRatio: function(t) { - var mt = (1-t), - t3 = t*t*t, - mt3 = mt*mt*mt, - bottom = t3 + mt3, - top = bottom - 1; - return abs(top/bottom); - }, - - drawQuadratic: function(api, curve) { - var labels = ["start","t=0.5","end"]; - - api.reset(); - - api.setColor("lightblue"); - api.drawGrid(10,10); - - api.setFill("black"); - api.setColor("black"); - api.lpts.forEach((p,i) => { - api.drawCircle(p,3); - api.text(labels[i], p, {x:5, y:2}); - }); - - if(api.lpts.length === 3) { - var S = api.lpts[0], - E = api.lpts[2], - B = api.lpts[1], - C = { - x: (S.x + E.x)/2, - y: (S.y + E.y)/2 - }; - api.setColor("blue"); - api.drawLine(S, E); - api.drawLine(B, C); - api.drawCircle(C, 3); - var ratio = this.getQRatio(0.5), - A = { - x: B.x + (B.x-C.x)/ratio, - y: B.y + (B.y-C.y)/ratio - }; - curve = new api.Bezier([S, A, E]); - api.setColor("lightgrey"); - api.drawLine(A, B); - api.drawLine(A, S); - api.drawLine(A, E); - api.setColor("black"); - api.drawCircle(A, 1); - api.drawCurve(curve); - } - }, - - drawCubic: function(api, curve) { - var labels = ["start","t=0.5","end"]; - - api.reset(); - - api.setFill("black"); - api.setColor("black"); - api.lpts.forEach((p,i) => { - api.drawCircle(p,3); - api.text(labels[i], p, {x:5, y:2}); - }); - - api.setColor("lightblue"); - api.drawGrid(10,10); - - if(api.lpts.length === 3) { - var S = api.lpts[0], - E = api.lpts[2], - B = api.lpts[1], - C = { - x: (S.x + E.x)/2, - y: (S.y + E.y)/2 - }; - - api.setColor("blue"); - api.drawLine(S, E); - api.drawLine(B, C); - api.drawCircle(C, 1); - - var ratio = this.getCRatio(0.5), - A = { - x: B.x + (B.x-C.x)/ratio, - y: B.y + (B.y-C.y)/ratio - }, - selen = api.utils.dist(S,E), - bclen_min = selen/8, - bclen = api.utils.dist(B,C), - aesthetics = 4, - be12dist = bclen_min + bclen/aesthetics, - bx = be12dist * (E.x-S.x)/selen, - by = be12dist * (E.y-S.y)/selen, - e1 = { - x: B.x - bx, - y: B.y - by - }, - e2 = { - x: B.x + bx, - y: B.y + by - }, - - v1 = { - x: A.x + (e1.x-A.x)*2, - y: A.y + (e1.y-A.y)*2 - }, - v2 = { - x: A.x + (e2.x-A.x)*2, - y: A.y + (e2.y-A.y)*2 - }, - - nc1 = { - x: S.x + (v1.x-S.x)*2, - y: S.y + (v1.y-S.y)*2 - }, - nc2 = { - x: E.x + (v2.x-E.x)*2, - y: E.y + (v2.y-E.y)*2 - }; - - curve = new api.Bezier([S, nc1, nc2, E]); - api.drawLine(e1, e2); - api.setColor("lightgrey"); - api.drawLine(A, C); - api.drawLine(A, v1); - api.drawLine(A, v2); - api.drawLine(S, nc1); - api.drawLine(E, nc2); - api.drawLine(nc1, nc2); - api.setColor("black"); - api.drawCircle(A, 1); - api.drawCircle(nc1, 1); - api.drawCircle(nc2, 1); - api.drawCurve(curve); - } - } -}; diff --git a/docs/chapters/pointcurves/quadratic.js b/docs/chapters/pointcurves/quadratic.js new file mode 100644 index 00000000..4ebc3201 --- /dev/null +++ b/docs/chapters/pointcurves/quadratic.js @@ -0,0 +1,45 @@ +let points; + +setup() { + points = [ + {x: 0.25 * this.width, y: 0.25 * this.height}, + {x: 0.5 * this.width, y: 0.5 * this.height}, + {x: 0.25 * this.width, y: 0.75 * this.height}, + ]; + setMovable(points); +} + +draw() { + clear(); + let [S, B, E] = points, t; + + if (points.length === 3) { + let d1 = dist(S.x, S.y, B.x, B.y); + let d2 = dist(E.x, E.y, B.x, B.y); + t = d1 / (d1 + d2); + + let {A, C} = Bezier.getABC(2, ...points, t); + let curve = new Bezier(this, [S, A, E]); + + curve.drawSkeleton(`lightblue`); + curve.drawCurve(`grey`); + setColor(`lightblue`); + circle(A.x, A.y, 2); + circle(C.x, C.y, 2); + text(`A`, A.x+5, A.y+5); + line(S.x,S.y,E.x,E.y); + line(C.x,C.y,A.x,A.y); + text(`C`, C.x+5, C.y+15); + } + + setColor(`black`); + let lbl = [`Start`, `B`, `End`]; + points.forEach((p,i) => { + circle(p.x, p.y, 3); + text(lbl[i], p.x+5, p.y+5); + }); + + if (points.length === 3) { + text(` with t=${t.toFixed(2)}`, B.x + 10, B.y+5); + } +} diff --git a/docs/images/chapters/pointcurves/067a3df30e32708fc0d13f8eb78c0b05.png b/docs/images/chapters/pointcurves/067a3df30e32708fc0d13f8eb78c0b05.png new file mode 100644 index 00000000..9adff154 Binary files /dev/null and b/docs/images/chapters/pointcurves/067a3df30e32708fc0d13f8eb78c0b05.png differ diff --git a/docs/images/chapters/pointcurves/3c7516c16a5dea95df741f4263cecd1c.svg b/docs/images/chapters/pointcurves/3c7516c16a5dea95df741f4263cecd1c.svg new file mode 100644 index 00000000..026650d3 --- /dev/null +++ b/docs/images/chapters/pointcurves/3c7516c16a5dea95df741f4263cecd1c.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/pointcurves/55d4f7ed095dfea8f9772208abc83b51.svg b/docs/images/chapters/pointcurves/55d4f7ed095dfea8f9772208abc83b51.svg new file mode 100644 index 00000000..2c9b9d06 --- /dev/null +++ b/docs/images/chapters/pointcurves/55d4f7ed095dfea8f9772208abc83b51.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/pointcurves/6f0e2b6494d7dae2ea79a46a499d7ed4.svg b/docs/images/chapters/pointcurves/6f0e2b6494d7dae2ea79a46a499d7ed4.svg new file mode 100644 index 00000000..8030f952 --- /dev/null +++ b/docs/images/chapters/pointcurves/6f0e2b6494d7dae2ea79a46a499d7ed4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/pointcurves/8b0e8e79ba09916a9af27b33f8a1919f.png b/docs/images/chapters/pointcurves/8b0e8e79ba09916a9af27b33f8a1919f.png new file mode 100644 index 00000000..a7fa3c66 Binary files /dev/null and b/docs/images/chapters/pointcurves/8b0e8e79ba09916a9af27b33f8a1919f.png differ diff --git a/docs/images/chapters/pointcurves/93ee12566f93f241ad0970acd5505367.png b/docs/images/chapters/pointcurves/93ee12566f93f241ad0970acd5505367.png new file mode 100644 index 00000000..0d52c607 Binary files /dev/null and b/docs/images/chapters/pointcurves/93ee12566f93f241ad0970acd5505367.png differ diff --git a/docs/images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg b/docs/images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg new file mode 100644 index 00000000..72bc8a8a --- /dev/null +++ b/docs/images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/pointcurves/eab6ea46fa93030e03ec0ef7deb571dc.png b/docs/images/chapters/pointcurves/eab6ea46fa93030e03ec0ef7deb571dc.png new file mode 100644 index 00000000..9d0ec70a Binary files /dev/null and b/docs/images/chapters/pointcurves/eab6ea46fa93030e03ec0ef7deb571dc.png differ diff --git a/docs/index.html b/docs/index.html index 7646c13b..59313ec6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1681,14 +1681,45 @@ for (coordinate, index) in LUT:

Creating a curve from three points

-

Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a t value, so we can figure out where point 'C' needs to be.

-

The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped t to simply be 0.5, lets us bypass some maths, since a t value of 0.5 always puts C in the middle of the start--end line segment)

- +

Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although

+

For quadratic curves, things are pretty easy: technically we need a t value in order to compute the ratio function used in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and B point, and B and end point as a ratio, using

+ +

With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using A as our curve's control point:

+ + + + Scripts are disabled. Showing fallback image. + -

For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped t to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:

- +

For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.

+

That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:

+ + + + Scripts are disabled. Showing fallback image. + -

In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which t value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of t and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.

+

With that covered, we now also know the tangent line to our point B, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points e1 and e2 on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable t value, and then our e1 and e2 coordinates must obey the standard de Casteljau rule for linear interpolation:

+ +

Where d is the total length of the line segment from e1 to e2. So how long do we make that? There are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the length of the baseline". This forces e1 and e2 to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually look like a line. Nice! The last thing we'll need to do is make sure to flip the sign of d depending on which side of the baseline our B is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the atan2 function:

+ +

This angle φ will be between 0 and π if B is "above" the baseline (rotating all three points so that the start is on the left and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value d:

+ +

The result of this approach looks as follows:

+ + + + Scripts are disabled. Showing fallback image. + + +

It is important to remember that even though we're using a circular arc to come up with decent e1 and e2 terms, we're not trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; more on that later), we're only trying to come up with some reasonable e1 and e2 points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:

+ + + + Scripts are disabled. Showing fallback image. + + +

That looks perfectly servicable!

diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 8c1b26ae..9a967849 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -1678,14 +1678,45 @@ for (coordinate, index) in LUT:

Creating a curve from three points

-

Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a t value, so we can figure out where point 'C' needs to be.

-

The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped t to simply be 0.5, lets us bypass some maths, since a t value of 0.5 always puts C in the middle of the start--end line segment)

- +

Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although

+

For quadratic curves, things are pretty easy: technically we need a t value in order to compute the ratio function used in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and B point, and B and end point as a ratio, using

+ +

With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using A as our curve's control point:

+ + + + Scripts are disabled. Showing fallback image. + -

For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped t to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:

- +

For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.

+

That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:

+ + + + Scripts are disabled. Showing fallback image. + -

In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which t value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of t and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.

+

With that covered, we now also know the tangent line to our point B, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points e1 and e2 on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable t value, and then our e1 and e2 coordinates must obey the standard de Casteljau rule for linear interpolation:

+ +

Where d is the total length of the line segment from e1 to e2. So how long do we make that? There are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the length of the baseline". This forces e1 and e2 to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually look like a line. Nice! The last thing we'll need to do is make sure to flip the sign of d depending on which side of the baseline our B is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the atan2 function:

+ +

This angle φ will be between 0 and π if B is "above" the baseline (rotating all three points so that the start is on the left and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value d:

+ +

The result of this approach looks as follows:

+ + + + Scripts are disabled. Showing fallback image. + + +

It is important to remember that even though we're using a circular arc to come up with decent e1 and e2 terms, we're not trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; more on that later), we're only trying to come up with some reasonable e1 and e2 points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:

+ + + + Scripts are disabled. Showing fallback image. + + +

That looks perfectly servicable!

diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 57007b89..a2ab9261 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -465,11 +465,18 @@ class GraphicsAPI extends BaseAPI { } /** - * Draw a circle around a Point + * Draw a circle */ circle(x, y, r) { + this.arc(x, y, r, 0, this.TAU); + } + + /** + * Draw a circular arc + */ + arc(x, y, r, s, e) { this.ctx.beginPath(); - this.ctx.arc(x, y, r, 0, this.TAU); + this.ctx.arc(x, y, r, s, e); this.ctx.fill(); this.ctx.stroke(); } diff --git a/docs/js/custom-element/lib/bezierjs/bezier.js b/docs/js/custom-element/lib/bezierjs/bezier.js index 6ae40ce5..2c1b6c63 100644 --- a/docs/js/custom-element/lib/bezierjs/bezier.js +++ b/docs/js/custom-element/lib/bezierjs/bezier.js @@ -238,15 +238,20 @@ class Bezier { return utils.length(this.derivative.bind(this)); } - getABC(t, B) { - let S = this.points[0]; - let E = this.points[this.order]; - let ret = getABC(this.order, S, B || this.get(t), E, t); + static getABC(order = 2, S, B, E, t = 0.5) { + let ret = getABC(order, S, B, E, t); ret.S = S; ret.E = E; return ret; } + 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; diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index b198d81b..4acc0e23 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -1672,14 +1672,45 @@ for (coordinate, index) in LUT:

Creating a curve from three points

-

Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a t value, so we can figure out where point 'C' needs to be.

-

The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped t to simply be 0.5, lets us bypass some maths, since a t value of 0.5 always puts C in the middle of the start--end line segment)

- +

Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although

+

For quadratic curves, things are pretty easy: technically we need a t value in order to compute the ratio function used in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and B point, and B and end point as a ratio, using

+ +

With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using A as our curve's control point:

+ + + + Scripts are disabled. Showing fallback image. + -

For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped t to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:

- +

For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.

+

That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:

+ + + + Scripts are disabled. Showing fallback image. + -

In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which t value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of t and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.

+

With that covered, we now also know the tangent line to our point B, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points e1 and e2 on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable t value, and then our e1 and e2 coordinates must obey the standard de Casteljau rule for linear interpolation:

+ +

Where d is the total length of the line segment from e1 to e2. So how long do we make that? There are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the length of the baseline". This forces e1 and e2 to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually look like a line. Nice! The last thing we'll need to do is make sure to flip the sign of d depending on which side of the baseline our B is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the atan2 function:

+ +

This angle φ will be between 0 and π if B is "above" the baseline (rotating all three points so that the start is on the left and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value d:

+ +

The result of this approach looks as follows:

+ + + + Scripts are disabled. Showing fallback image. + + +

It is important to remember that even though we're using a circular arc to come up with decent e1 and e2 terms, we're not trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; more on that later), we're only trying to come up with some reasonable e1 and e2 points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:

+ + + + Scripts are disabled. Showing fallback image. + + +

That looks perfectly servicable!