diff --git a/docs/chapters/abc/abc.js b/docs/chapters/abc/abc.js index c811c8f8..880bab3d 100644 --- a/docs/chapters/abc/abc.js +++ b/docs/chapters/abc/abc.js @@ -45,10 +45,10 @@ draw() { line(p1.x, p1.y, p2.x, p2.y); } - this.drawABCdata(t, A, B, C); + this.drawABCdata(t, A, B, C, hull); } -drawABCdata(t, A, B, C) { +drawABCdata(t, A, B, C, hull) { // show the lines between the A/B/C values setStroke(`#00FF00`); line(A.x, A.y, B.x, B.y); @@ -59,10 +59,23 @@ drawABCdata(t, A, B, C) { // with their associated labels setFill(`black`); + text(`Using t = ${t.toFixed(2)}`, this.width/2, 10, CENTER); + + setTextStroke(`white`, 4); text(`A`, 10 + A.x, A.y); - text(`B (t = ${t.toFixed(2)})`, 10 + B.x, B.y); + text(`B`, 10 + B.x, B.y); text(`C`, 10 + C.x, C.y); + if(curve.order === 2) { + text(`e1`, hull[3].x, hull[3].y+3, CENTER); + text(`e2`, hull[4].x, hull[4].y+3, CENTER); + } else { + text(`e1`, hull[7].x, hull[7].y+3, CENTER); + text(`e2`, hull[8].x, hull[8].y+3, CENTER); + text(`v1`, hull[4].x, hull[4].y+3, CENTER); + text(`v2`, hull[6].x, hull[6].y+3, CENTER); + } + // and show the distance ratio, which we see does not change irrespective of whether A/B/C change. const d1 = dist(A.x, A.y, B.x, B.y); const d2 = dist(B.x, B.y, C.x, C.y); diff --git a/docs/chapters/abc/content.en-GB.md b/docs/chapters/abc/content.en-GB.md index 73170fea..cfba136a 100644 --- a/docs/chapters/abc/content.en-GB.md +++ b/docs/chapters/abc/content.en-GB.md @@ -1,6 +1,6 @@ # The projection identity -De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mould" a curve, by picking it up at some point, and dragging that point around to change the curve's shape. +De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point around to change the curve's shape. How does that work? Succinctly: we run de Casteljau's algorithm in reverse! @@ -24,6 +24,8 @@ So these graphics show us several things: 1. a point at the tip of the curve construction's "hat": let's call that `A`, as well as 2. our on-curve point give our chosen `t` value: let's call that `B`, and finally, 3. a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that `C`. +4. for both qudratic and cubic curves, two points `e1` and `e2`, which represent the single-to-last step in de Casteljau's algorithm: in the last step, we find `B` at `(1-t) * e1 + t * e2`. +4. for cubic curves, also the points `v1` and `v2`, which together with `A` represent the first step in de Casteljau's algorithm: in the next step, we find `e1` and `e2`. These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on the curve with some `t` value, the ratio of distances from A to B and B to C is fixed: if some `t` value sets up a C that is 20% away from the start and 80% away from the end, then _it doesn't matter where the start, end, or control points are_; for that `t` value, `C` will *always* lie at 20% from the start and 80% from the end point. Go ahead, pick an on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move, and so neither will C, and if you move either start or end point, C will move but its relative position will not change. @@ -71,4 +73,22 @@ Which now leaves us with some powerful tools: given thee points (start, end, and A = B - \frac{C - B}{ratio(t)} = B + \frac{B - C}{ratio(t)} \] -So: if we have a curve's start and end point, then for any `t` value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections. +With `A` found, finding `e1` and `e2` for quadratic curves is a matter of running the linear interpolation with `t` between start and `A` to yield `e1`, and between `A` and end to yield `e2`. For cubic curves, there is no single pair of points that can act as `e1` and `e2`: as long as the distance ratio between `e1` to `B` and `B` to `e2` is the Bézier ratio `(1-t):t`, we can reverse engineer `v1` and `v2`: + +\[ + \left \{ \begin{aligned} + v_1 &= A' - \frac{A' - e_1}{1 - t} \\ + v_2 &= A' - \frac{A' - e_2}{t} + \end{aligned} \right . +\] + +And then reverse engineer the curve's control control points: + +\[ + \left \{ \begin{aligned} + C_1' &= start + \frac{v_1 - start}{t} \\ + C_2' &= end + \frac{v_2 - end}{1 - t} + \end{aligned} \right . +\] + +So: if we have a curve's start and end point, then for any `t` value we implicitly know all the ABC values, which (combined with an educated guess on appropriate `e1` and `e2` coordinates for cubic curves) gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mold" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next few sections. diff --git a/docs/chapters/catmullmoulding/content.en-GB.md b/docs/chapters/catmullmolding/content.en-GB.md similarity index 100% rename from docs/chapters/catmullmoulding/content.en-GB.md rename to docs/chapters/catmullmolding/content.en-GB.md diff --git a/docs/chapters/catmullmoulding/handler.js b/docs/chapters/catmullmolding/handler.js similarity index 100% rename from docs/chapters/catmullmoulding/handler.js rename to docs/chapters/catmullmolding/handler.js diff --git a/docs/chapters/molding/content.en-GB.md b/docs/chapters/molding/content.en-GB.md new file mode 100644 index 00000000..4f660148 --- /dev/null +++ b/docs/chapters/molding/content.en-GB.md @@ -0,0 +1,37 @@ +# Molding a curve + +Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around. + +For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a `t` value and initial `B` coordinate. We don't even need the latter: with our `t` value and "whever the cursor is" as target `B`, we can compute the associated `C`: + +\[ + C = u(t)_{q} \cdot Start + \left ( 1-u(t)_{q} \right ) \cdot End +\] + +And then the associated `A`: + +\[ + A = B - \frac{C - B}{ratio(t)_{q}} = B + \frac{B - C}{ratio(t)_{q}} +\] + +And we're done, because that's our new quadratic control point! + + + +As before, cubic curves are a bit more work, because while it's easy to find our initial `t` value and ABC values, getting those all-important `e1` and `e2` coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate `t` value ourselves, which allowed us to find appropriate `e1` and `e2` coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own `t` value, and its own `e1` and `e2` values, and those may not make sense for the rest of the curve. + +For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its `t` value and `e1`/`e2` coordinates: + + + +That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve. + +One way to combat this might be to combine the above approach with the approach from the [creating curves](#pointcurves) section: generate both the "unchanged `t`/`e1`/`e2`" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised `t` value, and then interpolating between those two curves: + + + + + +The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it interpolates with a bias towards "preserving `t`/`e1`/`e2`" closer to the original point, and bias towards "idealised" form the further away we move our point, with anything that's further than our falloff distance simply _being_ the idealised curve. We don't even try to interpolate at that point. + +A more advanced way to try to smooth things out is to implement _continuous_ molding, where we constantly update the curve as we move around, and constantly change what our `B` point is, based on constantly projecting the cursor on the curve _as we're updating it_ - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation (with a reasonable distance) will do for now! diff --git a/docs/chapters/moulding/decasteljau.js b/docs/chapters/molding/decasteljau.js similarity index 100% rename from docs/chapters/moulding/decasteljau.js rename to docs/chapters/molding/decasteljau.js diff --git a/docs/chapters/molding/molding.js b/docs/chapters/molding/molding.js new file mode 100644 index 00000000..ec0adad2 --- /dev/null +++ b/docs/chapters/molding/molding.js @@ -0,0 +1,262 @@ +let curve, utils = Bezier.getUtils(); + +setup() { + setPanelCount(3); + const type = this.type = this.parameters.type ?? `quadratic`; + curve = type === `quadratic` ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this); + this.position = {x:0,y:0}; + setMovable(curve.points, [this.position]); + if (this.parameters.interpolated) { + setSlider(`.slide-control`, `falloff`, 100); + } +} + +draw() { + clear(); + + curve.drawSkeleton(); + curve.drawCurve(); + curve.drawPoints(); + this.drawPosition(); + + nextPanel(); + + curve.drawSkeleton(`lightblue`); + curve.drawCurve(`lightblue`); + curve.points.forEach(p => circle(p.x, p.y, 2)); + this.drawMark(); + + nextPanel(); + + this.drawResult(); +} + +drawPosition() { + if (!this.position) return; + + setColor(`blue`); + let p = this.position.projection; + if (!this.mark) { + p = this.position.projection = curve.project( + this.position.x, + this.position.y + ) + this.position.x = p.x; + this.position.y = p.y; + } + circle(p.x, p.y, 3); +} + +drawMark() { + if (!this.mark) return; + if (this.type === `quadratic`) { + this.drawQuadraticMark(); + } else { + this.drawCubicMark(); + } +} + +drawQuadraticMark() { + let {B, t} = this.mark; + setFill(`black`); + text(`t: ${t.toFixed(5)}`, this.panelWidth/2, 15, CENTER); + + let {A, C, S, E} = curve.getABC(t, B); + setColor(`lightblue`); + line(S.x, S.y, E.x, E.y); + line(A.x, A.y, C.x, C.y); + + const lbl = [`A`, `B`, `C`]; + [A,B,C].forEach((p,i) => { + circle(p.x, p.y, 3); + text(lbl[i], p.x + 10, p.y); + }); + + + if (this.currentPoint) { + let {A,B,C,S,E} = curve.getABC(t, this.position); + setColor(`purple`); + line(A.x, A.y, C.x, C.y); + line(S.x, S.y, A.x, A.y); + line(E.x, E.y, A.x, A.y); + [A,B,C].forEach(p => circle(p.x, p.y, 3)); + + noFill(); + circle(B.x, B.y, 5); + this.molded = new Bezier(this, [S,A,E]); + } +} + +drawCubicMark() { + const S = curve.points[0], + E = curve.points[curve.order], + {B, t, e1, e2} = this.mark, + org = curve.getABC(t, B), + nB = this.position, + d1 = { x: e1.x - B.x, y: e1.y - B.y }, + d2 = { x: e2.x - B.x, y: e2.y - B.y }, + ne1 = { x: nB.x + d1.x, y: nB.y + d1.y }, + ne2 = { x: nB.x + d2.x, y: nB.y + d2.y }, + {A, C} = curve.getABC(t, nB), + {v1, v2, C1, C2} = this.deriveControlPoints(S, A, E, ne1, ne2, t); + + if (this.parameters.interpolated) { + const ideal = this.getIdealisedCurve(S, nB, E); + this.ideal = new Bezier(this, [ideal.S, ideal.C1, ideal.C2, ideal.E]); + } + + setColor(`black`); + text(`t: ${t}`, this.panelWidth/2, 20, CENTER); + + setColor(`lightblue`); + line(S.x,S.y,E.x,E.y); + line(org.C.x,org.C.y,org.A.x,org.A.y); + circle(org.A.x, org.A.y, 3); + circle(org.B.x, org.B.y, 3); + circle(org.C.x, org.C.y, 3); + text(`A`, org.A.x + 5, org.A.y); + text(`B`, org.B.x + 5, org.B.y); + text(`C`, org.C.x + 5, org.C.y); + + setColor(`purple`); + circle(A.x, A.y, 3); + circle(nB.x, nB.y, 3); + circle(C.x, C.y, 3); + circle(ne1.x, ne1.y, 2); + circle(ne2.x, ne2.y, 2); + + line(v1.x, v1.y, A.x, A.y); + line(v2.x, v2.y, A.x, A.y); + line(S.x,S.y,C1.x,C1.y); + line(E.x,E.y,C2.x,C2.y); + line(C2.x,C2.y,C1.x,C1.y); + line(A.x,A.y,C.x,C.y); + line(ne1.x, ne1.y, ne2.x, ne2.y); + + noFill(); + circle(nB.x, nB.y, 5); + + this.molded = new Bezier(this, [S,C1,C2,E]); +} + +deriveControlPoints(S, A, E, e1, e2, t) { + // And then use those to derive the correct v1/v2/C1/C2 coordinates + const v1 = { + x: A.x - (A.x - e1.x)/(1-t), + y: A.y - (A.y - e1.y)/(1-t) + }; + const v2 = { + x: A.x - (A.x - e2.x)/t, + y: A.y - (A.y - e2.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 {v1, v2, C1, C2}; +} + +getIdealisedCurve(p1, p2, p3) { + const c = utils.getccenter(p1, p2, p3), + d1 = dist(p1.x, p1.y, p2.x, p2.y), + d2 = dist(p3.x, p3.y, p2.x, p2.y), + t = d1 / (d1 + d2), + { A, B, C, S, E } = Bezier.getABC(3, p1, p2, p3, t), + angle = ( atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x) + TAU ) % TAU, + bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3, + de1 = t * bc, + de2 = (1-t) * bc, + 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, + e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy}, + e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy }, + {v1, v2, C1, C2} = this.deriveControlPoints(S, A, E, e1, e2, t); + + return {A,B,C,S,E,e1,e2,v1,v2,C1,C2}; +} + +drawResult() { + let last = curve; + if (this.molded) last = this.molded; + + last.drawSkeleton(`lightblue`); + last.drawCurve(this.parameters.interpolated ? `lightblue` : `black`); + last.points.forEach(p => circle(p.x, p.y, 2)); + + if (this.mark) { + let t = this.mark.t; + let B = last.get(t); + circle(B.x, B.y, 3); + + if (this.ideal) { + let d = dist(this.mark.B.x, this.mark.B.y, this.position.x, this.position.y); + let t = min(this.falloff, d) / this.falloff; + this.ideal.drawCurve(`lightblue`); + let iC1 = { + x: (1-t) * last.points[1].x + t * this.ideal.points[1].x, + y: (1-t) * last.points[1].y + t * this.ideal.points[1].y + }; + let iC2 = { + x: (1-t) * last.points[2].x + t * this.ideal.points[2].x, + y: (1-t) * last.points[2].y + t * this.ideal.points[2].y + }; + this.interpolated = new Bezier(this, [last.points[0], iC1, iC2, last.points[3]]); + this.interpolated.drawCurve(); + } + } +} + +onMouseDown() { + if (this.currentPoint !== this.position) { + this.mark = false; + this.position.projection = false; + } + else if (this.position.projection) { + let t = this.position.projection.t; + if (this.type === `quadratic`) { + this.mark = { + t, B: this.position.projection, + }; + } else { + let struts = curve.getStrutPoints(t); + let m = this.mark = { + t, B: this.position.projection, + e1: struts[7], + e2: struts[8] + }; + m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y}; + m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y}; + } + } + redraw(); +} + +onMouseMove() { + if (!this.currentPoint && !this.mark) { + this.position.x = this.cursor.x; + this.position.y = this.cursor.y; + } + redraw(); +} + +onMouseUp() { + this.mark = false; + if (this.molded) { + curve = this.interpolated ?? this.molded; + this.interpolated = false; + this.molded = false; + resetMovable(curve.points, [this.position]); + } + redraw(); +} diff --git a/docs/chapters/moulding/content.en-GB.md b/docs/chapters/moulding/content.en-GB.md deleted file mode 100644 index 8f52339d..00000000 --- a/docs/chapters/moulding/content.en-GB.md +++ /dev/null @@ -1,83 +0,0 @@ -# Manipulating a curve - -Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points. - - - -Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point `B` and its corresponding t-value, and the original points `A` and `C` for that t-value, in light coloring, as well as the new `A'`, `B'`, and `C'` (although of course the `C` coordinates are the same ones, because that's the defining feature of point `C`) based on where you're dragging point `B` to, in purple. - -Since we know the new point `B'`, and the "new" point `C'` as well as the `t` value, we know our new point A' has to be: - -\[ - A' = B' - \frac{C - B'}{ratio(t)} = B' + \frac{B' - C}{ratio(t)} -\] - -For quadratic curves, this means we're done, since the new point `A'` is equivalent to the new quadratic control point. - -For cubic curves, we need to do a little more work, because while computing a new `A'` is exactly the same as before, we're not quite done once we've done so. For cubic curves, `B` has not just an associated `t` value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean: - - - - - -In addition to the `A`, `B`, and `C` values, we also see the points `e1` and `e2`, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points `v1` and `v2`, which we can construct when we know our ABC values enriched with `e1` and `e2`: - -\[ - \left \{ \begin{aligned} - v_1 &= A' - \frac{A' - e_1}{1 - t} \\ - v_2 &= A' - \frac{A' - e_2}{t} - \end{aligned} \right . -\] - -After which computing the new control points is straight-forward: - -\[ - \left \{ \begin{aligned} - C_1' &= start + \frac{v_1 - start}{t} \\ - C_2' &= end + \frac{v_2 - end}{1 - t} - \end{aligned} \right . -\] - -So let's put that into practice: - - - -So that looks pretty good, but you may not like having `e1` and `e2` stay the same distances away from `B'` while moving the point around, and want to rearrange those to lead to "cleaner looking" curve manipulation. Unfortunately, there are so many differen ways in which we can do this that figuring out "good looking" alternatives, given what the curve is being manipulated for, could be an entire book on its own... so we're only going to look at one way that you might effect alternative `e1` and `e2` points, based on the idea of rotating a vector. - -If we treat point `B` as a "a vector originating at `C`" then we can treat the points `e1` and `e2` as offets (let's call these `d1` and `d2`) of that vector, where: - -\[ - \left \{ \begin{aligned} - e_1 &= B + d_1 \\ - e_2 &= B + d_2 - \end{aligned} \right . -\] - -Which means that: - -\[ - \left \{ \begin{aligned} - d_1 &= e_1 - B\\ - d_2 &= e_2 - B - \end{aligned} \right . -\] - -Now, if we now `B` to some new coordinate `B'` we can treat that "moving of the coordinate" as a rotation and scaling of the vector for `B` instead. If the new point `B'` is the same distance away from `C` as `B` was, this is a pure rotation, but otherwise the length of the vector has decreased or increased by some factor. - -We can use both those values to change where `e1` and `e2` end up, and thus how our curve moulding "feels", by placing new `e1'` and `e2'` where: - -\[ - \left \{ \begin{aligned} - angle &= atan2(B_y-C_y,B_x-C_x) - atan2(B_y\prime-C.y, B_x\prime-C.x) \\ - e_1' &= B' + scale \cdot rotate(d_1, B', angle) \\ - e_2' &= B' + scale \cdot rotate(d_2, B', angle) - \end{aligned} \right . -\] - -Here, the `rotate()` function rotates a vector (in this case `d1` or `d2`) around some point (in this case, `B'`), by some angle (in this case, the angle by which we rotated our original `B` to become `B'`). So what does _that_ look like? - - - -As you can see, this is both better, and worse, depending on what you're trying to do with the curve, and there are many different ways in which you can try to change `e1` and `e2` such that they behave "as users would expect them to" based on the context in which you're implementing curve moulding. You might want to add reflections when `B'` crosses the baseline, or even some kind of weight-swapping when `B'` crosses the midline (perpendicular to the baseline, at its mid point), and instead of scaling both points with respects to `C`, you might want to scale them to coordinates 1/2rd and 2/3rd along the baseline, etc. etc. - -There are too many options to go over here, so: the best behaviour is, of course, the behaviour _you_ think is best, and it might be a lot of work to find that and/or implement that! diff --git a/docs/chapters/moulding/moulding.js b/docs/chapters/moulding/moulding.js deleted file mode 100644 index b9b075d8..00000000 --- a/docs/chapters/moulding/moulding.js +++ /dev/null @@ -1,217 +0,0 @@ -let curve, utils = Bezier.getUtils(); - -setup() { - setPanelCount(3); - const type = this.type = this.parameters.type ?? `quadratic`; - curve = type === `quadratic` ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this); - this.position = {x:0,y:0}; - setMovable(curve.points, [this.position]); -} - -draw() { - clear(); - - curve.drawSkeleton(); - curve.drawCurve(); - curve.drawPoints(); - this.drawPosition(); - - nextPanel(); - - curve.drawSkeleton(`lightblue`); - curve.drawCurve(`lightblue`); - curve.points.forEach(p => circle(p.x, p.y, 2)); - this.drawMark(); - - nextPanel(); - - this.drawResult(); -} - -drawPosition() { - if (!this.position) return; - - setColor(`blue`); - let p = this.position.projection; - if (!this.mark) { - p = this.position.projection = curve.project( - this.position.x, - this.position.y - ) - this.position.x = p.x; - this.position.y = p.y; - } - circle(p.x, p.y, 3); -} - -drawMark() { - if (!this.mark) return; - if (this.type === `quadratic`) { - this.drawQuadraticMark(); - } else { - this.drawCubicMark(); - } -} - -drawQuadraticMark() { - let {B, t} = this.mark; - setFill(`black`); - text(`t = ${t.toFixed(2)}`, B.x + 5, B.y + 10); - - let {A, C, S, E} = curve.getABC(t, B); - setColor(`lightblue`); - line(S.x, S.y, E.x, E.y); - line(A.x, A.y, C.x, C.y); - - const lbl = [`A`, `B`, `C`]; - [A,B,C].forEach((p,i) => { - circle(p.x, p.y, 3); - text(lbl[i], p.x + 10, p.y); - }); - - if (this.currentPoint) { - let {A,B,C,S,E} = curve.getABC(t, this.position); - setColor(`purple`); - line(A.x, A.y, C.x, C.y); - line(S.x, S.y, A.x, A.y); - line(E.x, E.y, A.x, A.y); - [A,B,C].forEach(p => circle(p.x, p.y, 3)); - - noFill(); - circle(B.x, B.y, 5); - this.moulded = new Bezier(this, [S,A,E]); - } -} - -drawCubicMark() { - let {B, t, e1, e2, d1, d2} = this.mark; - let oB = B; - - setFill(`black`); - text(`t = ${t.toFixed(2)}`, B.x + 5, B.y + 10); - - let {A, C, S, E} = curve.getABC(this.mark.t, B); - let olen = dist(B.x, B.y, C.x, C.y); - setColor(`lightblue`); - line(S.x, S.y, E.x, E.y); - line(A.x, A.y, C.x, C.y); - - const lbl = [`A`, `B`, `C`, `e1`, `e2`]; - [A,B,C,e1,e2].forEach((p,i) => { - circle(p.x, p.y, 3); - text(lbl[i], p.x + 10, p.y); - }); - - if (this.currentPoint) { - let {A,B,C,S,E} = curve.getABC(this.mark.t, this.position); - let st1 = { x: B.x + d1.x, y: B.y + d1.y }; - let st2 = { x: B.x + d2.x, y: B.y + d2.y }; - - if (this.parameters.alternative) { - let nlen = dist(B.x, B.y, C.x, C.y); - let scale = nlen/olen; - let angle = atan2(B.y-C.y, B.x-C.x) - atan2(oB.y-C.y, oB.x-C.x); - - st1 = { - x: B.x + scale * d1.x * cos(angle) - scale * d1.y * sin(angle), - y: B.y + scale * d1.x * sin(angle) + scale * d1.y * cos(angle) - }; - - st2 = { - x: B.x + scale * d2.x * cos(angle) - scale * d2.y * sin(angle), - y: B.y + scale * d2.x * sin(angle) + scale * d2.y * cos(angle) - }; - } - - e1 = st1; - e2 = st2; - - setColor(`purple`); - line(A.x, A.y, C.x, C.y); - line(e1.x, e1.y, e2.x, e2.y); - - let v1 = { - x: A.x - (A.x - e1.x)/(1-t), - y: A.y - (A.y - e1.y)/(1-t) - }; - - let v2 = { - x: A.x - (A.x - e2.x)/t, - y: A.y - (A.y - e2.y)/t - }; - - let C1 = { - x: S.x + (v1.x - S.x) / t, - y: S.y + (v1.y - S.y) / t - }; - let C2 = { - x: E.x + (v2.x - E.x) / (1-t), - y: E.y + (v2.y - E.y) / (1-t) - }; - - [A,B,C,e1,e2,v1,v2,C1,C2].forEach(p => circle(p.x, p.y, 3)); - - noFill(); - circle(B.x, B.y, 5); - this.moulded = new Bezier(this, [S,C1,C2,E]); - } -} - -drawResult() { - let last = curve; - if (this.moulded) last = this.moulded; - - last.drawSkeleton(`lightblue`); - last.drawCurve(`black`); - last.points.forEach(p => circle(p.x, p.y, 2)); - - if (this.mark) { - let t = this.mark.t; - let B = last.get(t); - circle(B.x, B.y, 3); - setFill(`black`); - text(`t = ${this.mark.t.toFixed(2)}`, B.x + 5, B.y + 10); - } -} - -onMouseDown() { - if (this.currentPoint !== this.position) { - this.mark = false; - this.position.projection = false; - } - else if (this.position.projection) { - let t = this.position.projection.t; - if (this.type === `quadratic`) { - this.mark = { - t, B: this.position.projection, - }; - } else { - let struts = curve.getStrutPoints(t); - let m = this.mark = { - t, B: this.position.projection, - e1: struts[7], - e2: struts[8] - }; - m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y}; - m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y}; - } - } - redraw(); -} - -onMouseMove() { - if (!this.currentPoint && !this.mark) { - this.position.x = this.cursor.x; - this.position.y = this.cursor.y; - } - redraw(); -} - -onMouseUp() { - this.mark = false; - if (this.moulded) { - curve = this.moulded; - resetMovable(curve.points, [this.position]); - } - redraw(); -} diff --git a/docs/chapters/pointcurves/circle.js b/docs/chapters/pointcurves/circle.js index 21e00b4c..a1efa103 100644 --- a/docs/chapters/pointcurves/circle.js +++ b/docs/chapters/pointcurves/circle.js @@ -131,7 +131,7 @@ showCurve(p1, p2, p3, c) { // Check which length we need to use for our e1-e2 segment, // corrected for whether B is "above" or "below" the baseline: - const angle = atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x), + const angle = ( atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x) + TAU ) % TAU, bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3, de1 = t * bc, de2 = (1-t) * bc; diff --git a/docs/chapters/pointcurves/content.en-GB.md b/docs/chapters/pointcurves/content.en-GB.md index 6698934c..e923205f 100644 --- a/docs/chapters/pointcurves/content.en-GB.md +++ b/docs/chapters/pointcurves/content.en-GB.md @@ -1,8 +1,8 @@ # 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, although +Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having the computer do the rest, to which the answer is: that's exactly what we can now do! -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 +For quadratic curves, things are pretty easy. Technically, we'll 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} @@ -34,7 +34,7 @@ With that covered, we now also know the tangent line to our point `B`, because t 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) + \phi = \left ( atan2(E_y-S_y, E_x-S_x) - atan2(B_y-S_y, B_x-S_x) + 2 \pi \right ) \textit{ mod } 2 \pi \] 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`: @@ -46,7 +46,6 @@ This angle φ will be between 0 and π if `B` is "above" the baseline (rotating \end{aligned} \right . \] - The result of this approach looks as follows: @@ -56,3 +55,5 @@ It is important to remember that even though we're using a circular arc to come That looks perfectly servicable! + +Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold" curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at implementing curve molding in the section after that, so read on! diff --git a/docs/chapters/projections/content.en-GB.md b/docs/chapters/projections/content.en-GB.md index 0e7c544f..7b8ad96c 100644 --- a/docs/chapters/projections/content.en-GB.md +++ b/docs/chapters/projections/content.en-GB.md @@ -1,6 +1,6 @@ # Projecting a point onto a Bézier curve -Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve? +Before we can move on to actual curve molding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve? If the Bézier curve is of low enough order, we might be able to [work out the maths for how to do this](https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html), and get a perfect `t` value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal `t` value using a [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm). First, we do a coarse distance-check based on `t` values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast: diff --git a/docs/chapters/toc.js b/docs/chapters/toc.js index c99c372d..ac08ca25 100644 --- a/docs/chapters/toc.js +++ b/docs/chapters/toc.js @@ -45,14 +45,14 @@ export default [ // curve manipulation 'abc', - 'projections', - 'moulding', 'pointcurves', + 'projections', + 'molding', 'curvefitting', // A quick foray into Catmull-Rom splines 'catmullconv', - 'catmullmoulding', + 'catmullmolding', // "things made of more than on curve" 'polybezier', diff --git a/docs/images/chapters/moulding/3c696e0364d61b1391695342707d6ccc.svg b/docs/images/chapters/abc/3c696e0364d61b1391695342707d6ccc.svg similarity index 100% rename from docs/images/chapters/moulding/3c696e0364d61b1391695342707d6ccc.svg rename to docs/images/chapters/abc/3c696e0364d61b1391695342707d6ccc.svg diff --git a/docs/images/chapters/abc/6e40975c21e70b73954a4dce02b9ba75.png b/docs/images/chapters/abc/6e40975c21e70b73954a4dce02b9ba75.png deleted file mode 100644 index c122d396..00000000 Binary files a/docs/images/chapters/abc/6e40975c21e70b73954a4dce02b9ba75.png and /dev/null differ diff --git a/docs/images/chapters/abc/7a69dd4350ddda5701712e1d3b46b863.png b/docs/images/chapters/abc/7a69dd4350ddda5701712e1d3b46b863.png new file mode 100644 index 00000000..ac11b71a Binary files /dev/null and b/docs/images/chapters/abc/7a69dd4350ddda5701712e1d3b46b863.png differ diff --git a/docs/images/chapters/moulding/bc245327e0b011712168bad1c48dfec4.svg b/docs/images/chapters/abc/bc245327e0b011712168bad1c48dfec4.svg similarity index 100% rename from docs/images/chapters/moulding/bc245327e0b011712168bad1c48dfec4.svg rename to docs/images/chapters/abc/bc245327e0b011712168bad1c48dfec4.svg diff --git a/docs/images/chapters/abc/d744a4955a3ff4e2d85760887ea923d4.png b/docs/images/chapters/abc/d744a4955a3ff4e2d85760887ea923d4.png deleted file mode 100644 index 44b075f1..00000000 Binary files a/docs/images/chapters/abc/d744a4955a3ff4e2d85760887ea923d4.png and /dev/null differ diff --git a/docs/images/chapters/abc/eeec7cf16fb22c666e0143a3a030731f.png b/docs/images/chapters/abc/eeec7cf16fb22c666e0143a3a030731f.png new file mode 100644 index 00000000..fa569001 Binary files /dev/null and b/docs/images/chapters/abc/eeec7cf16fb22c666e0143a3a030731f.png differ diff --git a/docs/images/chapters/aligning/31655f24b7dd8b8871687b6610d9ac0e.png b/docs/images/chapters/aligning/31655f24b7dd8b8871687b6610d9ac0e.png index 01a971af..a86f56a5 100644 Binary files a/docs/images/chapters/aligning/31655f24b7dd8b8871687b6610d9ac0e.png and b/docs/images/chapters/aligning/31655f24b7dd8b8871687b6610d9ac0e.png differ diff --git a/docs/images/chapters/aligning/b3ccd45a72c815388aee6515fe37a486.png b/docs/images/chapters/aligning/b3ccd45a72c815388aee6515fe37a486.png index 5671d2e0..1152dc0a 100644 Binary files a/docs/images/chapters/aligning/b3ccd45a72c815388aee6515fe37a486.png and b/docs/images/chapters/aligning/b3ccd45a72c815388aee6515fe37a486.png differ diff --git a/docs/images/chapters/arclength/0d7138b99f5986332a050a8479eefa57.png b/docs/images/chapters/arclength/0d7138b99f5986332a050a8479eefa57.png index fcbfc54a..dcccaa77 100644 Binary files a/docs/images/chapters/arclength/0d7138b99f5986332a050a8479eefa57.png and b/docs/images/chapters/arclength/0d7138b99f5986332a050a8479eefa57.png differ diff --git a/docs/images/chapters/arclength/195f9a3c60f8dfe977c6450d21968f69.png b/docs/images/chapters/arclength/195f9a3c60f8dfe977c6450d21968f69.png index fff0de45..2a392424 100644 Binary files a/docs/images/chapters/arclength/195f9a3c60f8dfe977c6450d21968f69.png and b/docs/images/chapters/arclength/195f9a3c60f8dfe977c6450d21968f69.png differ diff --git a/docs/images/chapters/arclength/475547c773a7279dc037c9ced2c8c6dc.png b/docs/images/chapters/arclength/475547c773a7279dc037c9ced2c8c6dc.png index 208738a8..84d089cf 100644 Binary files a/docs/images/chapters/arclength/475547c773a7279dc037c9ced2c8c6dc.png and b/docs/images/chapters/arclength/475547c773a7279dc037c9ced2c8c6dc.png differ diff --git a/docs/images/chapters/arclength/580c33f599b70de44b17c546098508aa.png b/docs/images/chapters/arclength/580c33f599b70de44b17c546098508aa.png index 7859e733..12e5f42f 100644 Binary files a/docs/images/chapters/arclength/580c33f599b70de44b17c546098508aa.png and b/docs/images/chapters/arclength/580c33f599b70de44b17c546098508aa.png differ diff --git a/docs/images/chapters/arclengthapprox/a040f6b7c7c33ada25ecfd1060726545.png b/docs/images/chapters/arclengthapprox/a040f6b7c7c33ada25ecfd1060726545.png index 7bc7ec66..a1d5d4eb 100644 Binary files a/docs/images/chapters/arclengthapprox/a040f6b7c7c33ada25ecfd1060726545.png and b/docs/images/chapters/arclengthapprox/a040f6b7c7c33ada25ecfd1060726545.png differ diff --git a/docs/images/chapters/arclengthapprox/c270144cc41e4ebc4b0b2331473530fa.png b/docs/images/chapters/arclengthapprox/c270144cc41e4ebc4b0b2331473530fa.png index 0c59fd08..95168964 100644 Binary files a/docs/images/chapters/arclengthapprox/c270144cc41e4ebc4b0b2331473530fa.png and b/docs/images/chapters/arclengthapprox/c270144cc41e4ebc4b0b2331473530fa.png differ diff --git a/docs/images/chapters/boundingbox/e2c621442e98e2cd20af7efe1cfb041f.png b/docs/images/chapters/boundingbox/e2c621442e98e2cd20af7efe1cfb041f.png index ef9b8cc0..63a2e5e3 100644 Binary files a/docs/images/chapters/boundingbox/e2c621442e98e2cd20af7efe1cfb041f.png and b/docs/images/chapters/boundingbox/e2c621442e98e2cd20af7efe1cfb041f.png differ diff --git a/docs/images/chapters/boundingbox/f8989a62ebec9d6f123291c146caab5b.png b/docs/images/chapters/boundingbox/f8989a62ebec9d6f123291c146caab5b.png index dd13a46c..244f786f 100644 Binary files a/docs/images/chapters/boundingbox/f8989a62ebec9d6f123291c146caab5b.png and b/docs/images/chapters/boundingbox/f8989a62ebec9d6f123291c146caab5b.png differ diff --git a/docs/images/chapters/circles/fe32474b4616ee9478e1308308f1b6bf.svg b/docs/images/chapters/circles/fe32474b4616ee9478e1308308f1b6bf.svg deleted file mode 100644 index 0a70cf4b..00000000 --- a/docs/images/chapters/circles/fe32474b4616ee9478e1308308f1b6bf.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/components/008604bc4c53bd7e0d97c99a67812ad1.png b/docs/images/chapters/components/008604bc4c53bd7e0d97c99a67812ad1.png index 992f0f7e..69508a37 100644 Binary files a/docs/images/chapters/components/008604bc4c53bd7e0d97c99a67812ad1.png and b/docs/images/chapters/components/008604bc4c53bd7e0d97c99a67812ad1.png differ diff --git a/docs/images/chapters/components/5214256129e6396e7ac1f1713fa9c88d.png b/docs/images/chapters/components/5214256129e6396e7ac1f1713fa9c88d.png index bdc5d9da..afcf5e50 100644 Binary files a/docs/images/chapters/components/5214256129e6396e7ac1f1713fa9c88d.png and b/docs/images/chapters/components/5214256129e6396e7ac1f1713fa9c88d.png differ diff --git a/docs/images/chapters/control/14cb9fbbaae9e7d87ae6bef3ea7a782e.svg b/docs/images/chapters/control/14cb9fbbaae9e7d87ae6bef3ea7a782e.svg deleted file mode 100644 index 7e17c4e5..00000000 --- a/docs/images/chapters/control/14cb9fbbaae9e7d87ae6bef3ea7a782e.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/control/49423783987ac4bc49fbe4c519dbc1d1.png b/docs/images/chapters/control/49423783987ac4bc49fbe4c519dbc1d1.png index aa6f0a67..c6757884 100644 Binary files a/docs/images/chapters/control/49423783987ac4bc49fbe4c519dbc1d1.png and b/docs/images/chapters/control/49423783987ac4bc49fbe4c519dbc1d1.png differ diff --git a/docs/images/chapters/control/afd21a9ba16965c2e7ec2d0d14892250.png b/docs/images/chapters/control/afd21a9ba16965c2e7ec2d0d14892250.png index bb0f9ac1..f49dedc5 100644 Binary files a/docs/images/chapters/control/afd21a9ba16965c2e7ec2d0d14892250.png and b/docs/images/chapters/control/afd21a9ba16965c2e7ec2d0d14892250.png differ diff --git a/docs/images/chapters/control/ecc15848fbe7b2176b0c89973f07c694.png b/docs/images/chapters/control/ecc15848fbe7b2176b0c89973f07c694.png index 35cc064a..a0511790 100644 Binary files a/docs/images/chapters/control/ecc15848fbe7b2176b0c89973f07c694.png and b/docs/images/chapters/control/ecc15848fbe7b2176b0c89973f07c694.png differ diff --git a/docs/images/chapters/curvature/5fcfb0572cae06717506c84768aa568c.png b/docs/images/chapters/curvature/5fcfb0572cae06717506c84768aa568c.png index 91159f32..c4f31da0 100644 Binary files a/docs/images/chapters/curvature/5fcfb0572cae06717506c84768aa568c.png and b/docs/images/chapters/curvature/5fcfb0572cae06717506c84768aa568c.png differ diff --git a/docs/images/chapters/curvature/876d7b2750d7c29068ac6181c3634d25.png b/docs/images/chapters/curvature/876d7b2750d7c29068ac6181c3634d25.png index 35f05f80..f221f2dd 100644 Binary files a/docs/images/chapters/curvature/876d7b2750d7c29068ac6181c3634d25.png and b/docs/images/chapters/curvature/876d7b2750d7c29068ac6181c3634d25.png differ diff --git a/docs/images/chapters/decasteljau/715d1d2eecc762d6bc1470954b145018.png b/docs/images/chapters/decasteljau/715d1d2eecc762d6bc1470954b145018.png index f8b12dad..9ba6a66d 100644 Binary files a/docs/images/chapters/decasteljau/715d1d2eecc762d6bc1470954b145018.png and b/docs/images/chapters/decasteljau/715d1d2eecc762d6bc1470954b145018.png differ diff --git a/docs/images/chapters/extended/391a61142c56b79260680aefb08cd9c4.png b/docs/images/chapters/extended/391a61142c56b79260680aefb08cd9c4.png index 11c418e7..8aa1860a 100644 Binary files a/docs/images/chapters/extended/391a61142c56b79260680aefb08cd9c4.png and b/docs/images/chapters/extended/391a61142c56b79260680aefb08cd9c4.png differ diff --git a/docs/images/chapters/extended/baeceec6e1587794b8b275a90d5d85e9.png b/docs/images/chapters/extended/baeceec6e1587794b8b275a90d5d85e9.png index a2560a58..64434967 100644 Binary files a/docs/images/chapters/extended/baeceec6e1587794b8b275a90d5d85e9.png and b/docs/images/chapters/extended/baeceec6e1587794b8b275a90d5d85e9.png differ diff --git a/docs/images/chapters/extremities/890406c7bc96904224f8f14940bf3e56.png b/docs/images/chapters/extremities/890406c7bc96904224f8f14940bf3e56.png index aeec8247..70a54af0 100644 Binary files a/docs/images/chapters/extremities/890406c7bc96904224f8f14940bf3e56.png and b/docs/images/chapters/extremities/890406c7bc96904224f8f14940bf3e56.png differ diff --git a/docs/images/chapters/extremities/9850eec01924deae7fda4400ce44270d.png b/docs/images/chapters/extremities/9850eec01924deae7fda4400ce44270d.png index 3e298859..b0b0ce30 100644 Binary files a/docs/images/chapters/extremities/9850eec01924deae7fda4400ce44270d.png and b/docs/images/chapters/extremities/9850eec01924deae7fda4400ce44270d.png differ diff --git a/docs/images/chapters/inflections/9e1ce3975100600d4979370851929b73.png b/docs/images/chapters/inflections/9e1ce3975100600d4979370851929b73.png index 1e066224..5d6aa317 100644 Binary files a/docs/images/chapters/inflections/9e1ce3975100600d4979370851929b73.png and b/docs/images/chapters/inflections/9e1ce3975100600d4979370851929b73.png differ diff --git a/docs/images/chapters/intersections/594c2df534a1736c03cd3a96ff4a9913.png b/docs/images/chapters/intersections/594c2df534a1736c03cd3a96ff4a9913.png index 04b94995..29b4cbc4 100644 Binary files a/docs/images/chapters/intersections/594c2df534a1736c03cd3a96ff4a9913.png and b/docs/images/chapters/intersections/594c2df534a1736c03cd3a96ff4a9913.png differ diff --git a/docs/images/chapters/intersections/dc26a6063dadc31d242f1c1c8f38bb5e.png b/docs/images/chapters/intersections/dc26a6063dadc31d242f1c1c8f38bb5e.png index 7b2c8a1b..2d639782 100644 Binary files a/docs/images/chapters/intersections/dc26a6063dadc31d242f1c1c8f38bb5e.png and b/docs/images/chapters/intersections/dc26a6063dadc31d242f1c1c8f38bb5e.png differ diff --git a/docs/images/chapters/introduction/54e9ec0600ac436b0e6f0c6b5005cf03.png b/docs/images/chapters/introduction/54e9ec0600ac436b0e6f0c6b5005cf03.png index b2609b3a..c9ca1415 100644 Binary files a/docs/images/chapters/introduction/54e9ec0600ac436b0e6f0c6b5005cf03.png and b/docs/images/chapters/introduction/54e9ec0600ac436b0e6f0c6b5005cf03.png differ diff --git a/docs/images/chapters/introduction/8d158a13e9a86969b99c64057644cbc6.png b/docs/images/chapters/introduction/8d158a13e9a86969b99c64057644cbc6.png index 52d2ab0f..f0fa6ba0 100644 Binary files a/docs/images/chapters/introduction/8d158a13e9a86969b99c64057644cbc6.png and b/docs/images/chapters/introduction/8d158a13e9a86969b99c64057644cbc6.png differ diff --git a/docs/images/chapters/matrixsplit/77a11d65d7cffc4b84a85c4bec837792.svg b/docs/images/chapters/matrixsplit/77a11d65d7cffc4b84a85c4bec837792.svg deleted file mode 100644 index 36cae683..00000000 --- a/docs/images/chapters/matrixsplit/77a11d65d7cffc4b84a85c4bec837792.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/molding/079d318ad693b6b17413a91f5de06be8.svg b/docs/images/chapters/molding/079d318ad693b6b17413a91f5de06be8.svg new file mode 100644 index 00000000..3328f2d2 --- /dev/null +++ b/docs/images/chapters/molding/079d318ad693b6b17413a91f5de06be8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/moulding/b9928928e1f622e66b59e4a59cfac925.png b/docs/images/chapters/molding/522f1edd37163772b81acb86d3a4f423.png similarity index 100% rename from docs/images/chapters/moulding/b9928928e1f622e66b59e4a59cfac925.png rename to docs/images/chapters/molding/522f1edd37163772b81acb86d3a4f423.png diff --git a/docs/images/chapters/moulding/adc7e0785dade356b62fadcd903e73d9.png b/docs/images/chapters/molding/6b91671c2962530b863ae0da5789a9cc.png similarity index 100% rename from docs/images/chapters/moulding/adc7e0785dade356b62fadcd903e73d9.png rename to docs/images/chapters/molding/6b91671c2962530b863ae0da5789a9cc.png diff --git a/docs/images/chapters/molding/82a99caec5f84fb26dce28277377c041.svg b/docs/images/chapters/molding/82a99caec5f84fb26dce28277377c041.svg new file mode 100644 index 00000000..a37ecc1c --- /dev/null +++ b/docs/images/chapters/molding/82a99caec5f84fb26dce28277377c041.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/molding/ea1656c068a60631135ad499e8a29453.png b/docs/images/chapters/molding/ea1656c068a60631135ad499e8a29453.png new file mode 100644 index 00000000..a1bd3855 Binary files /dev/null and b/docs/images/chapters/molding/ea1656c068a60631135ad499e8a29453.png differ diff --git a/docs/images/chapters/moulding/28e90ffa101453e4c030174d36d185a5.svg b/docs/images/chapters/moulding/28e90ffa101453e4c030174d36d185a5.svg deleted file mode 100644 index 00903138..00000000 --- a/docs/images/chapters/moulding/28e90ffa101453e4c030174d36d185a5.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/moulding/54e423d1eb61157abd3acffc5271c3ac.svg b/docs/images/chapters/moulding/54e423d1eb61157abd3acffc5271c3ac.svg deleted file mode 100644 index e2bf60e2..00000000 --- a/docs/images/chapters/moulding/54e423d1eb61157abd3acffc5271c3ac.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/moulding/65c9a6f8a210d41b18f20e4da1ba1403.svg b/docs/images/chapters/moulding/65c9a6f8a210d41b18f20e4da1ba1403.svg deleted file mode 100644 index 50b10a22..00000000 --- a/docs/images/chapters/moulding/65c9a6f8a210d41b18f20e4da1ba1403.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/moulding/7159e1e6eeab38c5aba225fe7553dbe6.svg b/docs/images/chapters/moulding/7159e1e6eeab38c5aba225fe7553dbe6.svg deleted file mode 100644 index 529e0673..00000000 --- a/docs/images/chapters/moulding/7159e1e6eeab38c5aba225fe7553dbe6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/moulding/db817c3899e954da6c882e4699a49353.png b/docs/images/chapters/moulding/db817c3899e954da6c882e4699a49353.png deleted file mode 100644 index 07b52bf4..00000000 Binary files a/docs/images/chapters/moulding/db817c3899e954da6c882e4699a49353.png and /dev/null differ diff --git a/docs/images/chapters/moulding/fbc7d78dd768eec4314eb4b5320f2ce5.png b/docs/images/chapters/moulding/fbc7d78dd768eec4314eb4b5320f2ce5.png deleted file mode 100644 index 1c013908..00000000 Binary files a/docs/images/chapters/moulding/fbc7d78dd768eec4314eb4b5320f2ce5.png and /dev/null differ diff --git a/docs/images/chapters/pointcurves/93ee12566f93f241ad0970acd5505367.png b/docs/images/chapters/pointcurves/173ea31517a72a927d561f121f0677db.png similarity index 100% rename from docs/images/chapters/pointcurves/93ee12566f93f241ad0970acd5505367.png rename to docs/images/chapters/pointcurves/173ea31517a72a927d561f121f0677db.png diff --git a/docs/images/chapters/pointcurves/8b0e8e79ba09916a9af27b33f8a1919f.png b/docs/images/chapters/pointcurves/8d045d352f5017b65e60620b92d7ae29.png similarity index 100% rename from docs/images/chapters/pointcurves/8b0e8e79ba09916a9af27b33f8a1919f.png rename to docs/images/chapters/pointcurves/8d045d352f5017b65e60620b92d7ae29.png diff --git a/docs/images/chapters/pointcurves/9203537b7dca98ebb2d7017c76100fde.svg b/docs/images/chapters/pointcurves/9203537b7dca98ebb2d7017c76100fde.svg new file mode 100644 index 00000000..68d0b578 --- /dev/null +++ b/docs/images/chapters/pointcurves/9203537b7dca98ebb2d7017c76100fde.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg b/docs/images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg deleted file mode 100644 index 72bc8a8a..00000000 --- a/docs/images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png b/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png index 8ed9c7d4..29cd4246 100644 Binary files a/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png and b/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png differ diff --git a/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png b/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png index 8ed9c7d4..29cd4246 100644 Binary files a/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png and b/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png differ diff --git a/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png b/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png index 68d6ee86..9872628f 100644 Binary files a/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png and b/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png differ diff --git a/docs/images/chapters/reordering/387f931043aabd6c467985c568482636.png b/docs/images/chapters/reordering/387f931043aabd6c467985c568482636.png index 38b1af3d..d3e4eee8 100644 Binary files a/docs/images/chapters/reordering/387f931043aabd6c467985c568482636.png and b/docs/images/chapters/reordering/387f931043aabd6c467985c568482636.png differ diff --git a/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png b/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png index 5c45044c..4acb0222 100644 Binary files a/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png and b/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png differ diff --git a/docs/images/chapters/tightbounds/419415bee6ffd7598c035c42de09a94f.png b/docs/images/chapters/tightbounds/419415bee6ffd7598c035c42de09a94f.png index b87215b9..e2476be8 100644 Binary files a/docs/images/chapters/tightbounds/419415bee6ffd7598c035c42de09a94f.png and b/docs/images/chapters/tightbounds/419415bee6ffd7598c035c42de09a94f.png differ diff --git a/docs/images/chapters/tightbounds/ccc77ae1f57d7dd7ce4d5397fe1b140b.png b/docs/images/chapters/tightbounds/ccc77ae1f57d7dd7ce4d5397fe1b140b.png index 4c4d7d23..35d4eba1 100644 Binary files a/docs/images/chapters/tightbounds/ccc77ae1f57d7dd7ce4d5397fe1b140b.png and b/docs/images/chapters/tightbounds/ccc77ae1f57d7dd7ce4d5397fe1b140b.png differ diff --git a/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png b/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png index b310ab7b..b388009f 100644 Binary files a/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png and b/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png differ diff --git a/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png b/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png index 53b536b0..8af7840a 100644 Binary files a/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png and b/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png differ diff --git a/docs/images/chapters/weightcontrol/0ef06657f0540938686b9b35b249a22b.png b/docs/images/chapters/weightcontrol/0ef06657f0540938686b9b35b249a22b.png index 52d2ab0f..f0fa6ba0 100644 Binary files a/docs/images/chapters/weightcontrol/0ef06657f0540938686b9b35b249a22b.png and b/docs/images/chapters/weightcontrol/0ef06657f0540938686b9b35b249a22b.png differ diff --git a/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png b/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png index 6494d1ac..cb486f7e 100644 Binary files a/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png and b/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png differ diff --git a/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png b/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png index e1237568..05dc1d69 100644 Binary files a/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png and b/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png differ diff --git a/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png b/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png index 8b7276d3..10802f2c 100644 Binary files a/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png and b/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png differ diff --git a/docs/index.html b/docs/index.html index 59313ec6..2a6b7ba4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -95,12 +95,12 @@
  • Intersections
  • Curve/curve intersection
  • The projection identity
  • -
  • Projecting a point onto a Bézier curve
  • -
  • Manipulating a curve
  • Creating a curve from three points
  • +
  • Projecting a point onto a Bézier curve
  • +
  • Molding a curve
  • Curve fitting
  • Bézier curves and Catmull-Rom curves
  • -
  • Creating a Catmull-Rom curve from three points
  • +
  • Creating a Catmull-Rom curve from three points
  • Forming poly-Bézier curves
  • Boolean shape operations
  • Curve offsetting
  • @@ -197,12 +197,12 @@
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -219,7 +219,7 @@

    So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over those lines gives us two points, between which we can again perform linear interpolation, yielding a single point. And that point —and all points we can form in this way for all ratios taken together— form our Bézier curve:

    - + Scripts are disabled. Showing fallback image. @@ -246,7 +246,7 @@

    So, parametric curves don't define a y coordinate in terms of an x coordinate, like normal functions do, but they instead link the values to a "control" variable. If we vary the value of t, then with every change we get two values, which we can use as (x,y) coordinates in a graph. The above set of functions, for instance, generates points on a circle: We can range t from negative to positive infinity, and the resulting (x,y) coordinates will always lie on a circle with radius 1 around the origin (0,0). If we plot it for t from 0 to 5, we get this:

    - + Scripts are disabled. Showing fallback image. @@ -328,7 +328,7 @@ function Bezier(3,t):
    - + Scripts are disabled. Showing fallback image. @@ -336,7 +336,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -344,7 +344,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -353,13 +353,13 @@ function Bezier(3,t):

    Also shown is the interpolation function for a 15th order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.

    If we want to change the curve, we need to change the weights of each point, effectively changing the interpolations. The way to do this is about as straightforward as possible: just multiply each point with a value that changes its strength. These values are conventionally called "weights", and we can add them to our original Bézier function:

    - +

    That looks complicated, but as it so happens, the "weights" are actually just the coordinate values we want our curve to have: for an nth order curve, w0 is our start coordinate, wn is our last coordinate, and everything in between is a controlling coordinate. Say we want a cubic curve that starts at (110,150), is controlled by (25,190) and (210,250) and ends at (210,30), we use this Bézier curve:

    Which gives us the curve we saw at the top of the article:

    - + Scripts are disabled. Showing fallback image. @@ -404,7 +404,7 @@ function Bezier(3,t,w[]):

    But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:

    - + Scripts are disabled. Showing fallback image. ratio 1 1.0
    @@ -462,12 +462,12 @@ function RationalBezier(3,t,w[],r[]):
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -519,7 +519,7 @@ function RationalBezier(3,t,w[],r[]):

    To see this in action, mouse-over the following sketch. Moving the mouse changes which curve point is explicitly evaluated using de Casteljau's algorithm, moving the cursor left-to-right (or, of course, right-to-left), shows you how a curve is generated using this approach.

    - + Scripts are disabled. Showing fallback image. @@ -560,7 +560,7 @@ function RationalBezier(3,t,w[],r[]):
    - + Scripts are disabled. Showing fallback image. @@ -568,7 +568,7 @@ function RationalBezier(3,t,w[],r[]): - + Scripts are disabled. Showing fallback image. @@ -604,7 +604,7 @@ function RationalBezier(3,t,w[],r[]):

    Using de Casteljau's algorithm, we can also find all the points we need to split up a Bézier curve into two, smaller curves, which taken together form the original curve. When we construct de Casteljau's skeleton for some value t, the procedure gives us all the points we need to split a curve at that t value: one curve is defined by all the inside skeleton points found prior to our on-curve point, with the other curve being defined by all the inside skeleton points after our on-curve point.

    - + Scripts are disabled. Showing fallback image. @@ -637,7 +637,7 @@ function drawCurve(points[], t):

    Splitting curves using matrices

    Another way to split curves is to exploit the matrix representation of a Bézier curve. In the section on matrices, we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves respectively: (we'll reverse the Bézier coefficients vector for legibility)

    - +

    and

    Let's say we want to split the curve at some point t = z, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we separate out the actual "point on the curve" information into a new matrix multiplication:

    @@ -727,7 +727,7 @@ function drawCurve(points[], t):

    And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.

    - + Scripts are disabled. Showing fallback image. @@ -802,12 +802,12 @@ treated as a sequence of three (elementary) shear operations. When we combine th
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -836,7 +836,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:

    - + Scripts are disabled. Showing fallback image. @@ -910,7 +910,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:

    - + Scripts are disabled. Showing fallback image. @@ -928,13 +928,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.

    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1070,14 +1070,14 @@ function getCubicRoots(pa, pb, pc, pd) {

    So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:

    - + Scripts are disabled. Showing fallback image.

    And for cubic curves, that means first and second derivatives, in red and purple respectively:

    - + Scripts are disabled. Showing fallback image. @@ -1095,12 +1095,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1123,12 +1123,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1141,12 +1141,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1186,7 +1186,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    Taking that into account, we compute t, we disregard any t value that isn't in the Bézier interval [0,1], and we now know at which t value(s) our curve will inflect.

    - + Scripts are disabled. Showing fallback image. @@ -1198,7 +1198,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:

    - + Scripts are disabled. Showing fallback image. @@ -1264,7 +1264,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:

    - + Scripts are disabled. Showing fallback image. @@ -1275,7 +1275,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

    - + Scripts are disabled. Showing fallback image. @@ -1302,7 +1302,7 @@ y = curve.get(t).y

    So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

    - + Scripts are disabled. Showing fallback image. @@ -1324,17 +1324,17 @@ y = curve.get(t).y
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1357,7 +1357,7 @@ y = curve.get(t).y

    If we use the Legendre-Gauss values for our C values (thickness for each strip) and t values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?

    - + Scripts are disabled. Showing fallback image. @@ -1370,7 +1370,7 @@ y = curve.get(t).y - + Scripts are disabled. Showing fallback image. @@ -1378,7 +1378,7 @@ y = curve.get(t).y - + Scripts are disabled. Showing fallback image. @@ -1425,7 +1425,7 @@ y = curve.get(t).y

    With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.

    - + Scripts are disabled. Showing fallback image. @@ -1434,7 +1434,7 @@ y = curve.get(t).y

    So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:

    - + Scripts are disabled. Showing fallback image. @@ -1449,7 +1449,7 @@ y = curve.get(t).y

    The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.

    - + Scripts are disabled. Showing fallback image. @@ -1457,7 +1457,7 @@ y = curve.get(t).y

    So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

    - + Scripts are disabled. Showing fallback image. @@ -1476,7 +1476,7 @@ y = curve.get(t).y

    The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).

    - + Scripts are disabled. Showing fallback image. @@ -1508,12 +1508,12 @@ lli = function(line1, line2):
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1540,7 +1540,7 @@ lli = function(line1, line2):

    (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

    - + Scripts are disabled. Showing fallback image. @@ -1553,7 +1553,7 @@ lli = function(line1, line2):

    The projection identity

    -

    De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mould" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.

    +

    De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.

    How does that work? Succinctly: we run de Casteljau's algorithm in reverse!

    In order to run de Casteljau's algorithm in reverse, we need a few basic things: a start and end point, a point on the curve that want to be moving around, which has an associated t value, and a point we've not explicitly talked about before, and as far as I know has no explicit name, but lives one iteration higher in the de Casteljau process then our on-curve point does. I like to call it "A" for reasons that will become obvious.

    So let's use graphics instead of text to see where this "A" is, because text only gets us so far: move the sliders for the following graphics to see what, given specific t value, our A coordinate is. As well as some other coordinates, which taken together let us derive a value that the graphics call "ratio": if you move the curve's points around, A, B, and C will move, what happens to that value?

    @@ -1561,14 +1561,14 @@ lli = function(line1, line2): - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1581,6 +1581,8 @@ lli = function(line1, line2):
  • a point at the tip of the curve construction's "hat": let's call that A, as well as
  • our on-curve point give our chosen t value: let's call that B, and finally,
  • a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that C.
  • +
  • for both qudratic and cubic curves, two points e1 and e2, which represent the single-to-last step in de Casteljau's algorithm: in the last step, we find B at (1-t) * e1 + t * e2.
  • +
  • for cubic curves, also the points v1 and v2, which together with A represent the first step in de Casteljau's algorithm: in the next step, we find e1 and e2.
  • These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on the curve with some t value, the ratio of distances from A to B and B to C is fixed: if some t value sets up a C that is 20% away from the start and 80% away from the end, then it doesn't matter where the start, end, or control points are; for that t value, C will always lie at 20% from the start and 80% from the end point. Go ahead, pick an on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move, and so neither will C, and if you move either start or end point, C will move but its relative position will not change.

    So, how can we compute C? We start with our observation that C always lies somewhere between the start and ends points, so logically C will have a function that interpolates between those two coordinates:

    @@ -1598,12 +1600,60 @@ lli = function(line1, line2):

    Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a t value, we can contruct curves: we can compute C using the start and end points, and our u(t) function, and once we have C, we can use our on-curve point (B) and the ratio(t) function to find A:

    -

    So: if we have a curve's start and end point, then for any t value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.

    +

    With A found, finding e1 and e2 for quadratic curves is a matter of running the linear interpolation with t between start and A to yield e1, and between A and end to yield e2. For cubic curves, there is no single pair of points that can act as e1 and e2: as long as the distance ratio between e1 to B and B to e2 is the Bézier ratio (1-t):t, we can reverse engineer v1 and v2:

    + +

    And then reverse engineer the curve's control control points:

    + +

    So: if we have a curve's start and end point, then for any t value we implicitly know all the ABC values, which (combined with an educated guess on appropriate e1 and e2 coordinates for cubic curves) gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mold" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next few sections.

    + +
    +
    +

    Creating a curve from three points

    +

    Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having the computer do the rest, to which the answer is: that's exactly what we can now do!

    +

    For quadratic curves, things are pretty easy. Technically, we'll 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 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. + + +

    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!

    +

    Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold" curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at implementing curve molding in the section after that, so read on!

    Projecting a point onto a Bézier curve

    -

    Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    +

    Before we can move on to actual curve molding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:

    p = some point to project onto the curve
     d = some initially huge value
    @@ -1622,104 +1672,45 @@ for (coordinate, index) in LUT:
     

    So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bézier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.

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

    Manipulating a curve

    -

    Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.

    - +
    +

    Molding a curve

    +

    Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around.

    +

    For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a t value and initial B coordinate. We don't even need the latter: with our t value and "whever the cursor is" as target B, we can compute the associated C:

    + +

    And then the associated A:

    + +

    And we're done, because that's our new quadratic control point!

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

    Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, and the original points A and C for that t-value, in light coloring, as well as the new A', B', and C' (although of course the C coordinates are the same ones, because that's the defining feature of point C) based on where you're dragging point B to, in purple.

    -

    Since we know the new point B', and the "new" point C' as well as the t value, we know our new point A' has to be:

    - -

    For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point.

    -

    For cubic curves, we need to do a little more work, because while computing a new A' is exactly the same as before, we're not quite done once we've done so. For cubic curves, B has not just an associated t value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:

    - +

    As before, cubic curves are a bit more work, because while it's easy to find our initial t value and ABC values, getting those all-important e1 and e2 coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate t value ourselves, which allowed us to find appropriate e1 and e2 coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own t value, and its own e1 and e2 values, and those may not make sense for the rest of the curve.

    +

    For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its t value and e1/e2 coordinates:

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

    That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.

    +

    One way to combat this might be to combine the above approach with the approach from the creating curves section: generate both the "unchanged t/e1/e2" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised t value, and then interpolating between those two curves:

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

    In addition to the A, B, and C values, we also see the points e1 and e2, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points v1 and v2, which we can construct when we know our ABC values enriched with e1 and e2:

    - -

    After which computing the new control points is straight-forward:

    - -

    So let's put that into practice:

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

    So that looks pretty good, but you may not like having e1 and e2 stay the same distances away from B' while moving the point around, and want to rearrange those to lead to "cleaner looking" curve manipulation. Unfortunately, there are so many differen ways in which we can do this that figuring out "good looking" alternatives, given what the curve is being manipulated for, could be an entire book on its own... so we're only going to look at one way that you might effect alternative e1 and e2 points, based on the idea of rotating a vector.

    -

    If we treat point B as a "a vector originating at C" then we can treat the points e1 and e2 as offets (let's call these d1 and d2) of that vector, where:

    - -

    Which means that:

    - -

    Now, if we now B to some new coordinate B' we can treat that "moving of the coordinate" as a rotation and scaling of the vector for B instead. If the new point B' is the same distance away from C as B was, this is a pure rotation, but otherwise the length of the vector has decreased or increased by some factor.

    -

    We can use both those values to change where e1 and e2 end up, and thus how our curve moulding "feels", by placing new e1' and e2' where:

    - -

    Here, the rotate() function rotates a vector (in this case d1 or d2) around some point (in this case, B'), by some angle (in this case, the angle by which we rotated our original B to become B'). So what does that look like?

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

    As you can see, this is both better, and worse, depending on what you're trying to do with the curve, and there are many different ways in which you can try to change e1 and e2 such that they behave "as users would expect them to" based on the context in which you're implementing curve moulding. You might want to add reflections when B' crosses the baseline, or even some kind of weight-swapping when B' crosses the midline (perpendicular to the baseline, at its mid point), and instead of scaling both points with respects to C, you might want to scale them to coordinates 1/2rd and 2/3rd along the baseline, etc. etc.

    -

    There are too many options to go over here, so: the best behaviour is, of course, the behaviour you think is best, and it might be a lot of work to find that and/or implement that!

    - -
    -
    -

    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, 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 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. - - -

    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!

    +

    The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it interpolates with a bias towards "preserving t/e1/e2" closer to the original point, and bias towards "idealised" form the further away we move our point, with anything that's further than our falloff distance simply being the idealised curve. We don't even try to interpolate at that point.

    +

    A more advanced way to try to smooth things out is to implement continuous molding, where we constantly update the curve as we move around, and constantly change what our B point is, based on constantly projecting the cursor on the curve as we're updating it - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation (with a reasonable distance) will do for now!

    @@ -1886,8 +1877,8 @@ for (coordinate, index) in LUT:
    -
    -

    Creating a Catmull-Rom curve from three points

    +
    +

    Creating a Catmull-Rom curve from three points

    Now, we saw how to fit a Bézier curve to three points, but if Catmull-Rom curves go through points, why can't we just use those to do curve fitting, instead?

    As a matter of fact, we can, but there's a difference between the kind of curve fitting we did in the previous section, and the kind of curve fitting that we can do with Catmull-Rom curves. In the previous section we came up with a single curve that goes through three points. There was a decent amount of maths and computation involved, and the end result was three or four coordinates that described a single curve, depending on whether we were fitting a quadratic or cubic curve.

    Using Catmull-Rom curves, we need virtually no computation, but even though we end up with one Catmull-Rom curve of n points, in order to draw the equivalent curve using cubic Bézier curves we need a massive 3n-2 points (and that's without double-counting points that are shared by consecutive cubic curves).

    @@ -2051,7 +2042,7 @@ for (coordinate, index) in LUT:

    which we can then substitute in the expression for a:

    A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give the same x/y coordinates for both "a away from A" and "b away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:

    - +

    We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:

    Which, worked out for the x and y components, gives:

    diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 9a967849..073a3792 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -95,12 +95,12 @@
  • Intersections
  • Curve/curve intersection
  • The projection identity
  • -
  • Projecting a point onto a Bézier curve
  • -
  • Manipulating a curve
  • Creating a curve from three points
  • +
  • Projecting a point onto a Bézier curve
  • +
  • Molding a curve
  • Curve fitting
  • Bézier curves and Catmull-Rom curves
  • -
  • Creating a Catmull-Rom curve from three points
  • +
  • Creating a Catmull-Rom curve from three points
  • Forming poly-Bézier curves
  • Boolean shape operations
  • Curve offsetting
  • @@ -200,12 +200,12 @@
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -222,7 +222,7 @@

    では、実際に見てみましょう。下の図はインタラクティブになっています。上下キーで補間の比率が増減しますので、どうなるか確かめてみましょう。最初に3点があり、それを結んで2本の直線が引かれています。この直線の上でそれぞれ線形補間を行うと、2つの点が得られます。この2点の間でさらに線形補間を行うと、1つの点を得ることができます。そして、あらゆる比率に対して同様に点を求め、それをすべて集めると、このようにベジエ曲線ができるのです。

    - + Scripts are disabled. Showing fallback image. @@ -248,7 +248,7 @@

    というわけで、普通の関数ではy座標をx座標によって定義しますが、パラメトリック曲線ではそうではなく、座標の値を「制御」変数と結びつけます。tの値を変化させるたびに2つの値が変化するので、これをグラフ上の座標 (x,y)として使うことができます。例えば、先ほどの関数の組は円周上の点を生成します。負の無限大から正の無限大へとtを動かすと、得られる座標(x,y)は常に中心(0,0)・半径1の円の上に乗ります。tを0から5まで変化させてプロットした場合は、このようになります。

    - + Scripts are disabled. Showing fallback image. @@ -330,7 +330,7 @@ function Bezier(3,t):
    - + Scripts are disabled. Showing fallback image. @@ -338,7 +338,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -346,7 +346,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -402,7 +402,7 @@ function Bezier(3,t,w[]):

    But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:

    - + Scripts are disabled. Showing fallback image. ratio 1 1.0
    @@ -460,12 +460,12 @@ function RationalBezier(3,t,w[],r[]):
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -517,7 +517,7 @@ function RationalBezier(3,t,w[],r[]):

    下の図にマウスを乗せると、この様子を実際に見ることができます。ド・カステリョのアルゴリズムによって曲線上の点を明示的に計算していますが、マウスを動かすと求める点が変わります。マウスカーソルを左から右へ(もちろん、右から左へでも)動かせば、このアルゴリズムによって曲線が生成される様子がわかります。

    - + Scripts are disabled. Showing fallback image. @@ -557,7 +557,7 @@ function RationalBezier(3,t,w[],r[]):
    - + Scripts are disabled. Showing fallback image. @@ -565,7 +565,7 @@ function RationalBezier(3,t,w[],r[]): - + Scripts are disabled. Showing fallback image. @@ -601,7 +601,7 @@ function RationalBezier(3,t,w[],r[]):

    ベジエ曲線を分割して、繫ぎ合わせたときに元に戻るような小さい2曲線にしたい場合にも、ド・カステリョのアルゴリズムを使えば、これに必要な点をすべて求めることができます。ある値tに対してド・カステリョの骨格を組み立てると、そのtで曲線を分割する際に必要になる点がすべて得られます。骨格内部の点のうち、曲線上の点から見て手前側にある点によって一方の曲線が定義され、向こう側にある点によってもう一方の曲線が定義されます。

    - + Scripts are disabled. Showing fallback image. @@ -634,7 +634,7 @@ function drawCurve(points[], t):

    行列による曲線の分割

    曲線分割には、ベジエ曲線の行列表現を利用する方法もあります。行列についての節では、行列の乗算で曲線が表現できることを確認しました。特に2次・3次のベジエ曲線に関しては、それぞれ以下のような形になりました(読みやすさのため、ベジエの係数ベクトルを反転させています)。

    - +

    ならびに

    曲線をある点t = zで分割し、新しく2つの(自明ですが、より短い)ベジエ曲線を作ることを考えましょう。曲線の行列表現と線形代数を利用すると、この2つのベジエ曲線の座標を求めることができます。まず、実際の「曲線上の点」の情報を分解し、新しい行列の積のかたちにします。

    @@ -724,7 +724,7 @@ function drawCurve(points[], t):

    And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.

    - + Scripts are disabled. Showing fallback image. @@ -799,12 +799,12 @@ treated as a sequence of three (elementary) shear operations. When we combine th
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -833,7 +833,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:

    - + Scripts are disabled. Showing fallback image. @@ -907,7 +907,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:

    - + Scripts are disabled. Showing fallback image. @@ -925,13 +925,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.

    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1067,14 +1067,14 @@ function getCubicRoots(pa, pb, pc, pd) {

    So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:

    - + Scripts are disabled. Showing fallback image.

    And for cubic curves, that means first and second derivatives, in red and purple respectively:

    - + Scripts are disabled. Showing fallback image. @@ -1092,12 +1092,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1120,12 +1120,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1138,12 +1138,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1183,7 +1183,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    Taking that into account, we compute t, we disregard any t value that isn't in the Bézier interval [0,1], and we now know at which t value(s) our curve will inflect.

    - + Scripts are disabled. Showing fallback image. @@ -1195,7 +1195,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:

    - + Scripts are disabled. Showing fallback image. @@ -1261,7 +1261,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:

    - + Scripts are disabled. Showing fallback image. @@ -1272,7 +1272,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

    - + Scripts are disabled. Showing fallback image. @@ -1299,7 +1299,7 @@ y = curve.get(t).y

    So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

    - + Scripts are disabled. Showing fallback image. @@ -1321,17 +1321,17 @@ y = curve.get(t).y
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1354,7 +1354,7 @@ y = curve.get(t).y

    If we use the Legendre-Gauss values for our C values (thickness for each strip) and t values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?

    - + Scripts are disabled. Showing fallback image. @@ -1367,7 +1367,7 @@ y = curve.get(t).y - + Scripts are disabled. Showing fallback image. @@ -1375,7 +1375,7 @@ y = curve.get(t).y - + Scripts are disabled. Showing fallback image. @@ -1422,7 +1422,7 @@ y = curve.get(t).y

    With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.

    - + Scripts are disabled. Showing fallback image. @@ -1431,7 +1431,7 @@ y = curve.get(t).y

    So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:

    - + Scripts are disabled. Showing fallback image. @@ -1446,7 +1446,7 @@ y = curve.get(t).y

    The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.

    - + Scripts are disabled. Showing fallback image. @@ -1454,7 +1454,7 @@ y = curve.get(t).y

    So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

    - + Scripts are disabled. Showing fallback image. @@ -1473,7 +1473,7 @@ y = curve.get(t).y

    The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).

    - + Scripts are disabled. Showing fallback image. @@ -1505,12 +1505,12 @@ lli = function(line1, line2):
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1537,7 +1537,7 @@ lli = function(line1, line2):

    (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

    - + Scripts are disabled. Showing fallback image. @@ -1550,7 +1550,7 @@ lli = function(line1, line2):

    The projection identity

    -

    De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mould" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.

    +

    De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.

    How does that work? Succinctly: we run de Casteljau's algorithm in reverse!

    In order to run de Casteljau's algorithm in reverse, we need a few basic things: a start and end point, a point on the curve that want to be moving around, which has an associated t value, and a point we've not explicitly talked about before, and as far as I know has no explicit name, but lives one iteration higher in the de Casteljau process then our on-curve point does. I like to call it "A" for reasons that will become obvious.

    So let's use graphics instead of text to see where this "A" is, because text only gets us so far: move the sliders for the following graphics to see what, given specific t value, our A coordinate is. As well as some other coordinates, which taken together let us derive a value that the graphics call "ratio": if you move the curve's points around, A, B, and C will move, what happens to that value?

    @@ -1558,14 +1558,14 @@ lli = function(line1, line2): - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1578,6 +1578,8 @@ lli = function(line1, line2):
  • a point at the tip of the curve construction's "hat": let's call that A, as well as
  • our on-curve point give our chosen t value: let's call that B, and finally,
  • a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that C.
  • +
  • for both qudratic and cubic curves, two points e1 and e2, which represent the single-to-last step in de Casteljau's algorithm: in the last step, we find B at (1-t) * e1 + t * e2.
  • +
  • for cubic curves, also the points v1 and v2, which together with A represent the first step in de Casteljau's algorithm: in the next step, we find e1 and e2.
  • These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on the curve with some t value, the ratio of distances from A to B and B to C is fixed: if some t value sets up a C that is 20% away from the start and 80% away from the end, then it doesn't matter where the start, end, or control points are; for that t value, C will always lie at 20% from the start and 80% from the end point. Go ahead, pick an on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move, and so neither will C, and if you move either start or end point, C will move but its relative position will not change.

    So, how can we compute C? We start with our observation that C always lies somewhere between the start and ends points, so logically C will have a function that interpolates between those two coordinates:

    @@ -1595,12 +1597,60 @@ lli = function(line1, line2):

    Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a t value, we can contruct curves: we can compute C using the start and end points, and our u(t) function, and once we have C, we can use our on-curve point (B) and the ratio(t) function to find A:

    -

    So: if we have a curve's start and end point, then for any t value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.

    +

    With A found, finding e1 and e2 for quadratic curves is a matter of running the linear interpolation with t between start and A to yield e1, and between A and end to yield e2. For cubic curves, there is no single pair of points that can act as e1 and e2: as long as the distance ratio between e1 to B and B to e2 is the Bézier ratio (1-t):t, we can reverse engineer v1 and v2:

    + +

    And then reverse engineer the curve's control control points:

    + +

    So: if we have a curve's start and end point, then for any t value we implicitly know all the ABC values, which (combined with an educated guess on appropriate e1 and e2 coordinates for cubic curves) gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mold" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next few sections.

    + +
    +
    +

    Creating a curve from three points

    +

    Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having the computer do the rest, to which the answer is: that's exactly what we can now do!

    +

    For quadratic curves, things are pretty easy. Technically, we'll 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 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. + + +

    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!

    +

    Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold" curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at implementing curve molding in the section after that, so read on!

    Projecting a point onto a Bézier curve

    -

    Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    +

    Before we can move on to actual curve molding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:

    p = some point to project onto the curve
     d = some initially huge value
    @@ -1619,104 +1669,45 @@ for (coordinate, index) in LUT:
     

    So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bézier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.

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

    Manipulating a curve

    -

    Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.

    - +
    +

    Molding a curve

    +

    Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around.

    +

    For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a t value and initial B coordinate. We don't even need the latter: with our t value and "whever the cursor is" as target B, we can compute the associated C:

    + +

    And then the associated A:

    + +

    And we're done, because that's our new quadratic control point!

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

    Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, and the original points A and C for that t-value, in light coloring, as well as the new A', B', and C' (although of course the C coordinates are the same ones, because that's the defining feature of point C) based on where you're dragging point B to, in purple.

    -

    Since we know the new point B', and the "new" point C' as well as the t value, we know our new point A' has to be:

    - -

    For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point.

    -

    For cubic curves, we need to do a little more work, because while computing a new A' is exactly the same as before, we're not quite done once we've done so. For cubic curves, B has not just an associated t value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:

    - +

    As before, cubic curves are a bit more work, because while it's easy to find our initial t value and ABC values, getting those all-important e1 and e2 coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate t value ourselves, which allowed us to find appropriate e1 and e2 coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own t value, and its own e1 and e2 values, and those may not make sense for the rest of the curve.

    +

    For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its t value and e1/e2 coordinates:

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

    That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.

    +

    One way to combat this might be to combine the above approach with the approach from the creating curves section: generate both the "unchanged t/e1/e2" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised t value, and then interpolating between those two curves:

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

    In addition to the A, B, and C values, we also see the points e1 and e2, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points v1 and v2, which we can construct when we know our ABC values enriched with e1 and e2:

    - -

    After which computing the new control points is straight-forward:

    - -

    So let's put that into practice:

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

    So that looks pretty good, but you may not like having e1 and e2 stay the same distances away from B' while moving the point around, and want to rearrange those to lead to "cleaner looking" curve manipulation. Unfortunately, there are so many differen ways in which we can do this that figuring out "good looking" alternatives, given what the curve is being manipulated for, could be an entire book on its own... so we're only going to look at one way that you might effect alternative e1 and e2 points, based on the idea of rotating a vector.

    -

    If we treat point B as a "a vector originating at C" then we can treat the points e1 and e2 as offets (let's call these d1 and d2) of that vector, where:

    - -

    Which means that:

    - -

    Now, if we now B to some new coordinate B' we can treat that "moving of the coordinate" as a rotation and scaling of the vector for B instead. If the new point B' is the same distance away from C as B was, this is a pure rotation, but otherwise the length of the vector has decreased or increased by some factor.

    -

    We can use both those values to change where e1 and e2 end up, and thus how our curve moulding "feels", by placing new e1' and e2' where:

    - -

    Here, the rotate() function rotates a vector (in this case d1 or d2) around some point (in this case, B'), by some angle (in this case, the angle by which we rotated our original B to become B'). So what does that look like?

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

    As you can see, this is both better, and worse, depending on what you're trying to do with the curve, and there are many different ways in which you can try to change e1 and e2 such that they behave "as users would expect them to" based on the context in which you're implementing curve moulding. You might want to add reflections when B' crosses the baseline, or even some kind of weight-swapping when B' crosses the midline (perpendicular to the baseline, at its mid point), and instead of scaling both points with respects to C, you might want to scale them to coordinates 1/2rd and 2/3rd along the baseline, etc. etc.

    -

    There are too many options to go over here, so: the best behaviour is, of course, the behaviour you think is best, and it might be a lot of work to find that and/or implement that!

    - -
    -
    -

    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, 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 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. - - -

    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!

    +

    The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it interpolates with a bias towards "preserving t/e1/e2" closer to the original point, and bias towards "idealised" form the further away we move our point, with anything that's further than our falloff distance simply being the idealised curve. We don't even try to interpolate at that point.

    +

    A more advanced way to try to smooth things out is to implement continuous molding, where we constantly update the curve as we move around, and constantly change what our B point is, based on constantly projecting the cursor on the curve as we're updating it - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation (with a reasonable distance) will do for now!

    @@ -1883,8 +1874,8 @@ for (coordinate, index) in LUT:
    -
    -

    Creating a Catmull-Rom curve from three points

    +
    +

    Creating a Catmull-Rom curve from three points

    Now, we saw how to fit a Bézier curve to three points, but if Catmull-Rom curves go through points, why can't we just use those to do curve fitting, instead?

    As a matter of fact, we can, but there's a difference between the kind of curve fitting we did in the previous section, and the kind of curve fitting that we can do with Catmull-Rom curves. In the previous section we came up with a single curve that goes through three points. There was a decent amount of maths and computation involved, and the end result was three or four coordinates that described a single curve, depending on whether we were fitting a quadratic or cubic curve.

    Using Catmull-Rom curves, we need virtually no computation, but even though we end up with one Catmull-Rom curve of n points, in order to draw the equivalent curve using cubic Bézier curves we need a massive 3n-2 points (and that's without double-counting points that are shared by consecutive cubic curves).

    @@ -2048,7 +2039,7 @@ for (coordinate, index) in LUT:

    which we can then substitute in the expression for a:

    A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give the same x/y coordinates for both "a away from A" and "b away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:

    - +

    We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:

    Which, worked out for the x and y components, gives:

    diff --git a/docs/js/custom-element/graphics-element.css b/docs/js/custom-element/graphics-element.css index 1bfd4bf9..06fa603a 100644 --- a/docs/js/custom-element/graphics-element.css +++ b/docs/js/custom-element/graphics-element.css @@ -38,6 +38,7 @@ graphics-element:not(:defined) fallback-image { font-size: 60%; text-align: center; padding-bottom: 0.2em; + visibility:collapse; } /* @@ -48,7 +49,7 @@ graphics-element:not(:defined) fallback-image { graphics-element:not(:defined) fallback-image > img { display: block; - margin: 0.9em; + visibility:visible; } /* @@ -59,7 +60,6 @@ graphics-element:not(:defined) fallback-image > img { graphics-element:defined { display: inline-block; padding: 0.5em; - margin: 1em auto; justify-self: center; font-size: revert; text-align: revert; @@ -70,6 +70,6 @@ graphics-element:defined { the does NOT show anymore! */ -graphics-element:defined fallback-image { +graphics-element:defined fallback-image.loaded { display: none; } diff --git a/docs/js/custom-element/graphics-element.js b/docs/js/custom-element/graphics-element.js index 93a7645a..e40a4af5 100644 --- a/docs/js/custom-element/graphics-element.js +++ b/docs/js/custom-element/graphics-element.js @@ -16,6 +16,8 @@ CustomElement.register(class ProgramCode extends HTMLElement {}); * Our custom element */ class GraphicsElement extends CustomElement { + static DEBUG = false; + constructor() { super({ header: false, footer: false }); @@ -220,12 +222,14 @@ class GraphicsElement extends CustomElement { * can't actually find anywhere in the document or shadow DOM... */ printCodeDueToError() { - console.log( - this.code - .split(`\n`) - .map((l, pos) => `${pos + 1}: ${l}`) - .join(`\n`) - ); + if (GraphicsElement.DEBUG) { + console.log( + this.code + .split(`\n`) + .map((l, pos) => `${pos + 1}: ${l}`) + .join(`\n`) + ); + } } /** diff --git a/docs/js/custom-element/lib/bezierjs/bezier.js b/docs/js/custom-element/lib/bezierjs/bezier.js index 2c1b6c63..e793fc9b 100644 --- a/docs/js/custom-element/lib/bezierjs/bezier.js +++ b/docs/js/custom-element/lib/bezierjs/bezier.js @@ -16,25 +16,6 @@ const pi = Math.PI; // a zero coordinate, which is surprisingly useful const ZERO = { x: 0, y: 0, z: 0 }; -// TODO: figure out where this function goes, it has no reason to exist on its lonesome. -function getABC(n, S, B, E, t) { - if (typeof t === "undefined") { - t = 0.5; - } - const u = utils.projectionratio(t, n), - um = 1 - u, - C = { - x: u * S.x + um * E.x, - y: u * S.y + um * E.y, - }, - s = utils.abcratio(t, n), - A = { - x: B.x + (B.x - C.x) / s, - y: B.y + (B.y - C.y) / s, - }; - return { A, B, C }; -} - /** * Bezier curve constructor. * @@ -128,7 +109,7 @@ class Bezier { return new Bezier(p1, p2, p2); } // real fitting. - const abc = getABC(2, p1, p2, p3, t); + const abc = Bezier.getABC(2, p1, p2, p3, t); return new Bezier(p1, abc.A, p3); } @@ -136,7 +117,7 @@ class Bezier { if (typeof t === "undefined") { t = 0.5; } - const abc = getABC(3, S, B, E, t); + const abc = Bezier.getABC(3, S, B, E, t); if (typeof d1 === "undefined") { d1 = utils.dist(B, abc.C); } @@ -239,10 +220,18 @@ class Bezier { } 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; + 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) { diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index 4acc0e23..7c5bc62a 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -95,12 +95,12 @@
  • Intersections
  • Curve/curve intersection
  • The projection identity
  • -
  • Projecting a point onto a Bézier curve
  • -
  • Manipulating a curve
  • Creating a curve from three points
  • +
  • Projecting a point onto a Bézier curve
  • +
  • Molding a curve
  • Curve fitting
  • Bézier curves and Catmull-Rom curves
  • -
  • Creating a Catmull-Rom curve from three points
  • +
  • Creating a Catmull-Rom curve from three points
  • Forming poly-Bézier curves
  • Boolean shape operations
  • Curve offsetting
  • @@ -194,12 +194,12 @@
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -217,7 +217,7 @@ : - + Scripts are disabled. Showing fallback image. @@ -242,7 +242,7 @@

    所以,参数曲线不像一般函数那样,通过x坐标来定义y坐标,而是用一个“控制”变量将它们连接起来。如果改变t的值,每次变化时我们都能得到两个值,这可以作为图形中的(x,y)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使t在正负极值间变化,得到的输出(x,y)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出t从0到5时的值,将得到如下图像:

    - + Scripts are disabled. Showing fallback image. @@ -324,7 +324,7 @@ function Bezier(3,t):
    - + Scripts are disabled. Showing fallback image. @@ -332,7 +332,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -340,7 +340,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -349,7 +349,7 @@ function Bezier(3,t):

    上面有一张是15th阶的插值方程。如你所见,在所有控制点中,起点和终点对曲线形状的贡献比其他点更大些。

    如果我们要改变曲线,就需要改变每个点的权重,有效地改变插值。可以很直接地做到这个:只要用一个值乘以每个点,来改变它的强度。这个值照惯例称为“权重”,我们可以将它加入我们原始的贝塞尔函数:

    - +

    看起来很复杂,但实际上“权重”只是我们想让曲线所拥有的坐标值:对于一条nth阶曲线,w0是起始坐标,wn是终点坐标,中间的所有点都是控制点坐标。假设说一条曲线的起点为(120,160),终点为(220,40),并受点(35,200)和点(220,260)的控制,贝塞尔曲线方程就为:

    这就是我们在文章开头看到的曲线:

    @@ -396,7 +396,7 @@ function Bezier(3,t,w[]):

    But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:

    - + Scripts are disabled. Showing fallback image. ratio 1 1.0
    @@ -454,12 +454,12 @@ function RationalBezier(3,t,w[],r[]):
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -511,7 +511,7 @@ function RationalBezier(3,t,w[],r[]):

    我们通过实际操作来观察这个过程。在以下的图表中,移动鼠标来改变用de Casteljau算法计算得到的曲线点,左右移动鼠标,可以实时看到曲线是如何生成的。

    - + Scripts are disabled. Showing fallback image. @@ -551,7 +551,7 @@ function RationalBezier(3,t,w[],r[]):
    - + Scripts are disabled. Showing fallback image. @@ -559,7 +559,7 @@ function RationalBezier(3,t,w[],r[]): - + Scripts are disabled. Showing fallback image. @@ -595,7 +595,7 @@ function RationalBezier(3,t,w[],r[]):

    使用 de Casteljau 算法我们也可以将一条贝塞尔曲线分割成两条更小的曲线,二者拼接起来即可形成原来的曲线。当采用某个 t 值构造 de Casteljau 算法时,该过程会给到我们在 t 点分割曲线的所有点: 一条曲线包含该曲线上点之前的所有点,另一条曲线包含该曲线上点之后的所有点。

    - + Scripts are disabled. Showing fallback image. @@ -628,7 +628,7 @@ function drawCurve(points[], t):

    Splitting curves using matrices

    Another way to split curves is to exploit the matrix representation of a Bézier curve. In the section on matrices, we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves respectively: (we'll reverse the Bézier coefficients vector for legibility)

    - +

    and

    Let's say we want to split the curve at some point t = z, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we separate out the actual "point on the curve" information into a new matrix multiplication:

    @@ -718,7 +718,7 @@ function drawCurve(points[], t):

    And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.

    - + Scripts are disabled. Showing fallback image. @@ -793,12 +793,12 @@ treated as a sequence of three (elementary) shear operations. When we combine th
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -827,7 +827,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:

    - + Scripts are disabled. Showing fallback image. @@ -901,7 +901,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:

    - + Scripts are disabled. Showing fallback image. @@ -919,13 +919,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.

    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1061,14 +1061,14 @@ function getCubicRoots(pa, pb, pc, pd) {

    So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:

    - + Scripts are disabled. Showing fallback image.

    And for cubic curves, that means first and second derivatives, in red and purple respectively:

    - + Scripts are disabled. Showing fallback image. @@ -1086,12 +1086,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1114,12 +1114,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1132,12 +1132,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1177,7 +1177,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    Taking that into account, we compute t, we disregard any t value that isn't in the Bézier interval [0,1], and we now know at which t value(s) our curve will inflect.

    - + Scripts are disabled. Showing fallback image. @@ -1189,7 +1189,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:

    - + Scripts are disabled. Showing fallback image. @@ -1255,7 +1255,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:

    - + Scripts are disabled. Showing fallback image. @@ -1266,7 +1266,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

    - + Scripts are disabled. Showing fallback image. @@ -1293,7 +1293,7 @@ y = curve.get(t).y

    So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

    - + Scripts are disabled. Showing fallback image. @@ -1315,17 +1315,17 @@ y = curve.get(t).y
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1348,7 +1348,7 @@ y = curve.get(t).y

    If we use the Legendre-Gauss values for our C values (thickness for each strip) and t values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?

    - + Scripts are disabled. Showing fallback image. @@ -1361,7 +1361,7 @@ y = curve.get(t).y - + Scripts are disabled. Showing fallback image. @@ -1369,7 +1369,7 @@ y = curve.get(t).y - + Scripts are disabled. Showing fallback image. @@ -1416,7 +1416,7 @@ y = curve.get(t).y

    With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.

    - + Scripts are disabled. Showing fallback image. @@ -1425,7 +1425,7 @@ y = curve.get(t).y

    So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:

    - + Scripts are disabled. Showing fallback image. @@ -1440,7 +1440,7 @@ y = curve.get(t).y

    The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.

    - + Scripts are disabled. Showing fallback image. @@ -1448,7 +1448,7 @@ y = curve.get(t).y

    So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

    - + Scripts are disabled. Showing fallback image. @@ -1467,7 +1467,7 @@ y = curve.get(t).y

    The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).

    - + Scripts are disabled. Showing fallback image. @@ -1499,12 +1499,12 @@ lli = function(line1, line2):
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1531,7 +1531,7 @@ lli = function(line1, line2):

    (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

    - + Scripts are disabled. Showing fallback image. @@ -1544,7 +1544,7 @@ lli = function(line1, line2):

    The projection identity

    -

    De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mould" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.

    +

    De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.

    How does that work? Succinctly: we run de Casteljau's algorithm in reverse!

    In order to run de Casteljau's algorithm in reverse, we need a few basic things: a start and end point, a point on the curve that want to be moving around, which has an associated t value, and a point we've not explicitly talked about before, and as far as I know has no explicit name, but lives one iteration higher in the de Casteljau process then our on-curve point does. I like to call it "A" for reasons that will become obvious.

    So let's use graphics instead of text to see where this "A" is, because text only gets us so far: move the sliders for the following graphics to see what, given specific t value, our A coordinate is. As well as some other coordinates, which taken together let us derive a value that the graphics call "ratio": if you move the curve's points around, A, B, and C will move, what happens to that value?

    @@ -1552,14 +1552,14 @@ lli = function(line1, line2): - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1572,6 +1572,8 @@ lli = function(line1, line2):
  • a point at the tip of the curve construction's "hat": let's call that A, as well as
  • our on-curve point give our chosen t value: let's call that B, and finally,
  • a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that C.
  • +
  • for both qudratic and cubic curves, two points e1 and e2, which represent the single-to-last step in de Casteljau's algorithm: in the last step, we find B at (1-t) * e1 + t * e2.
  • +
  • for cubic curves, also the points v1 and v2, which together with A represent the first step in de Casteljau's algorithm: in the next step, we find e1 and e2.
  • These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on the curve with some t value, the ratio of distances from A to B and B to C is fixed: if some t value sets up a C that is 20% away from the start and 80% away from the end, then it doesn't matter where the start, end, or control points are; for that t value, C will always lie at 20% from the start and 80% from the end point. Go ahead, pick an on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move, and so neither will C, and if you move either start or end point, C will move but its relative position will not change.

    So, how can we compute C? We start with our observation that C always lies somewhere between the start and ends points, so logically C will have a function that interpolates between those two coordinates:

    @@ -1589,12 +1591,60 @@ lli = function(line1, line2):

    Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a t value, we can contruct curves: we can compute C using the start and end points, and our u(t) function, and once we have C, we can use our on-curve point (B) and the ratio(t) function to find A:

    -

    So: if we have a curve's start and end point, then for any t value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.

    +

    With A found, finding e1 and e2 for quadratic curves is a matter of running the linear interpolation with t between start and A to yield e1, and between A and end to yield e2. For cubic curves, there is no single pair of points that can act as e1 and e2: as long as the distance ratio between e1 to B and B to e2 is the Bézier ratio (1-t):t, we can reverse engineer v1 and v2:

    + +

    And then reverse engineer the curve's control control points:

    + +

    So: if we have a curve's start and end point, then for any t value we implicitly know all the ABC values, which (combined with an educated guess on appropriate e1 and e2 coordinates for cubic curves) gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mold" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next few sections.

    + +
    +
    +

    Creating a curve from three points

    +

    Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having the computer do the rest, to which the answer is: that's exactly what we can now do!

    +

    For quadratic curves, things are pretty easy. Technically, we'll 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 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. + + +

    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!

    +

    Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold" curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at implementing curve molding in the section after that, so read on!

    Projecting a point onto a Bézier curve

    -

    Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    +

    Before we can move on to actual curve molding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:

    p = some point to project onto the curve
     d = some initially huge value
    @@ -1613,104 +1663,45 @@ for (coordinate, index) in LUT:
     

    So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bézier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.

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

    Manipulating a curve

    -

    Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.

    - +
    +

    Molding a curve

    +

    Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around.

    +

    For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a t value and initial B coordinate. We don't even need the latter: with our t value and "whever the cursor is" as target B, we can compute the associated C:

    + +

    And then the associated A:

    + +

    And we're done, because that's our new quadratic control point!

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

    Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, and the original points A and C for that t-value, in light coloring, as well as the new A', B', and C' (although of course the C coordinates are the same ones, because that's the defining feature of point C) based on where you're dragging point B to, in purple.

    -

    Since we know the new point B', and the "new" point C' as well as the t value, we know our new point A' has to be:

    - -

    For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point.

    -

    For cubic curves, we need to do a little more work, because while computing a new A' is exactly the same as before, we're not quite done once we've done so. For cubic curves, B has not just an associated t value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:

    - +

    As before, cubic curves are a bit more work, because while it's easy to find our initial t value and ABC values, getting those all-important e1 and e2 coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate t value ourselves, which allowed us to find appropriate e1 and e2 coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own t value, and its own e1 and e2 values, and those may not make sense for the rest of the curve.

    +

    For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its t value and e1/e2 coordinates:

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

    That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.

    +

    One way to combat this might be to combine the above approach with the approach from the creating curves section: generate both the "unchanged t/e1/e2" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised t value, and then interpolating between those two curves:

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

    In addition to the A, B, and C values, we also see the points e1 and e2, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points v1 and v2, which we can construct when we know our ABC values enriched with e1 and e2:

    - -

    After which computing the new control points is straight-forward:

    - -

    So let's put that into practice:

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

    So that looks pretty good, but you may not like having e1 and e2 stay the same distances away from B' while moving the point around, and want to rearrange those to lead to "cleaner looking" curve manipulation. Unfortunately, there are so many differen ways in which we can do this that figuring out "good looking" alternatives, given what the curve is being manipulated for, could be an entire book on its own... so we're only going to look at one way that you might effect alternative e1 and e2 points, based on the idea of rotating a vector.

    -

    If we treat point B as a "a vector originating at C" then we can treat the points e1 and e2 as offets (let's call these d1 and d2) of that vector, where:

    - -

    Which means that:

    - -

    Now, if we now B to some new coordinate B' we can treat that "moving of the coordinate" as a rotation and scaling of the vector for B instead. If the new point B' is the same distance away from C as B was, this is a pure rotation, but otherwise the length of the vector has decreased or increased by some factor.

    -

    We can use both those values to change where e1 and e2 end up, and thus how our curve moulding "feels", by placing new e1' and e2' where:

    - -

    Here, the rotate() function rotates a vector (in this case d1 or d2) around some point (in this case, B'), by some angle (in this case, the angle by which we rotated our original B to become B'). So what does that look like?

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

    As you can see, this is both better, and worse, depending on what you're trying to do with the curve, and there are many different ways in which you can try to change e1 and e2 such that they behave "as users would expect them to" based on the context in which you're implementing curve moulding. You might want to add reflections when B' crosses the baseline, or even some kind of weight-swapping when B' crosses the midline (perpendicular to the baseline, at its mid point), and instead of scaling both points with respects to C, you might want to scale them to coordinates 1/2rd and 2/3rd along the baseline, etc. etc.

    -

    There are too many options to go over here, so: the best behaviour is, of course, the behaviour you think is best, and it might be a lot of work to find that and/or implement that!

    - -
    -
    -

    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, 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 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. - - -

    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!

    +

    The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it interpolates with a bias towards "preserving t/e1/e2" closer to the original point, and bias towards "idealised" form the further away we move our point, with anything that's further than our falloff distance simply being the idealised curve. We don't even try to interpolate at that point.

    +

    A more advanced way to try to smooth things out is to implement continuous molding, where we constantly update the curve as we move around, and constantly change what our B point is, based on constantly projecting the cursor on the curve as we're updating it - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation (with a reasonable distance) will do for now!

    @@ -1877,8 +1868,8 @@ for (coordinate, index) in LUT:
    -
    -

    Creating a Catmull-Rom curve from three points

    +
    +

    Creating a Catmull-Rom curve from three points

    Now, we saw how to fit a Bézier curve to three points, but if Catmull-Rom curves go through points, why can't we just use those to do curve fitting, instead?

    As a matter of fact, we can, but there's a difference between the kind of curve fitting we did in the previous section, and the kind of curve fitting that we can do with Catmull-Rom curves. In the previous section we came up with a single curve that goes through three points. There was a decent amount of maths and computation involved, and the end result was three or four coordinates that described a single curve, depending on whether we were fitting a quadratic or cubic curve.

    Using Catmull-Rom curves, we need virtually no computation, but even though we end up with one Catmull-Rom curve of n points, in order to draw the equivalent curve using cubic Bézier curves we need a massive 3n-2 points (and that's without double-counting points that are shared by consecutive cubic curves).

    @@ -2042,7 +2033,7 @@ for (coordinate, index) in LUT:

    which we can then substitute in the expression for a:

    A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give the same x/y coordinates for both "a away from A" and "b away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:

    - +

    We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:

    Which, worked out for the x and y components, gives:

    diff --git a/src/build/markdown/preprocess-graphics-element.js b/src/build/markdown/preprocess-graphics-element.js index 600e2abe..5cb56511 100644 --- a/src/build/markdown/preprocess-graphics-element.js +++ b/src/build/markdown/preprocess-graphics-element.js @@ -95,7 +95,7 @@ async function preprocessGraphicsElement(chapter, localeStrings, markdown) { const replacement = `width="${width}" height="${height}" src="${src}" ${remainder}> - + ${translate`disabledMessage`} `;