From 9434a71d340be1338e91943b0241a3cd25f417bd Mon Sep 17 00:00:00 2001 From: Pomax Date: Sat, 5 Sep 2020 22:50:12 -0700 Subject: [PATCH] bsplines --- docs/chapters/arcapproximation/arc.js | 101 +++ docs/chapters/arcapproximation/arcs.js | 30 + .../arcapproximation/content.en-GB.md | 42 +- docs/chapters/arcapproximation/handler.js | 184 ----- docs/chapters/bsplined/content.en-GB.md | 62 +- docs/chapters/bsplines/basic-sketch.js | 30 - docs/chapters/bsplines/basic.js | 57 ++ docs/chapters/bsplines/center-cut-bspline.js | 52 -- docs/chapters/bsplines/content.en-GB.md | 56 +- docs/chapters/bsplines/handler.js | 16 - .../chapters/bsplines/open-uniform-bspline.js | 45 -- .../bsplines/rational-uniform-bspline.js | 50 -- docs/chapters/bsplines/rational-uniform.js | 43 ++ docs/chapters/bsplines/reduced.js | 49 ++ docs/chapters/bsplines/uniform-bspline.js | 43 -- docs/chapters/bsplines/uniform.js | 41 ++ docs/chapters/circles_cubic/circle.js | 2 +- docs/chapters/circles_cubic/content.en-GB.md | 2 +- .../chapters/curveintersection/curve-curve.js | 5 +- .../6f30b487d0cb60a4caeed4a199c48253.png | Bin 0 -> 14689 bytes .../7c9cce8142fa3e85bb124520f40645ff.png | Bin 0 -> 10837 bytes ...g => 2708c130a4a45cef8c998c94da3dd2b5.svg} | 0 ...g => 6b7f07a5f80edaef59d07e86e9d9a668.svg} | 0 .../0f3451c711c0fe5d0b018aa4aa77d855.svg | 114 ++-- .../4c8f9814c50c708757eeb5a68afabb7f.svg | 242 +++---- .../5d3b04c3161a3429ce651bb7a5fa0399.png | Bin 0 -> 12786 bytes .../610232b8f7ce7ef3f0f012d55e385c6d.png | Bin 0 -> 20215 bytes .../763838ea6f9e6c6aa63ea5f9c6d9542f.svg | 190 +++--- .../7962d6fea86da6f53a7269fba30f0138.svg | 630 +++++++++--------- .../892209dad8fd1f839470dd061e870913.svg | 158 ++--- .../8caa3e8ff614ad9731b15dacaba98c3c.png | Bin 0 -> 13926 bytes .../93146ea89bb21999d9e18b57dd1bdd29.png | Bin 0 -> 12771 bytes .../adac18ea69cc58e01c8d5e15498e4aa6.svg | 170 +++-- .../cf45d1ea00d4866abc8a058b130299b4.svg | 398 +++++------ .../fc654445500dd595d6ae9de27a3dc46c.png | Bin 0 -> 17198 bytes .../3c6f863c77cc2100573bf71adaabc12e.png | Bin 12398 -> 0 bytes .../8676cce0d4394ae095c7e50be1238aa0.png | Bin 0 -> 11654 bytes ...g => 914e097fe4341697e05b6fd328cc4c91.png} | Bin docs/index.html | 142 ++-- docs/ja-JP/index.html | 142 ++-- docs/js/custom-element/api/graphics-api.js | 3 +- docs/js/custom-element/api/types/bspline.js | 85 +++ docs/js/custom-element/api/util/spline.js | 88 +++ docs/js/custom-element/graphics-element.js | 2 +- docs/zh-CN/index.html | 142 ++-- .../markdown/generate-graphics-module.js | 2 +- 46 files changed, 1795 insertions(+), 1623 deletions(-) create mode 100644 docs/chapters/arcapproximation/arc.js create mode 100644 docs/chapters/arcapproximation/arcs.js delete mode 100644 docs/chapters/arcapproximation/handler.js delete mode 100644 docs/chapters/bsplines/basic-sketch.js create mode 100644 docs/chapters/bsplines/basic.js delete mode 100644 docs/chapters/bsplines/center-cut-bspline.js delete mode 100644 docs/chapters/bsplines/handler.js delete mode 100644 docs/chapters/bsplines/open-uniform-bspline.js delete mode 100644 docs/chapters/bsplines/rational-uniform-bspline.js create mode 100644 docs/chapters/bsplines/rational-uniform.js create mode 100644 docs/chapters/bsplines/reduced.js delete mode 100644 docs/chapters/bsplines/uniform-bspline.js create mode 100644 docs/chapters/bsplines/uniform.js create mode 100644 docs/images/chapters/arcapproximation/6f30b487d0cb60a4caeed4a199c48253.png create mode 100644 docs/images/chapters/arcapproximation/7c9cce8142fa3e85bb124520f40645ff.png rename docs/images/chapters/bsplined/{c32c4cabe4193e4b4c5e1d0e46aacf72.svg => 2708c130a4a45cef8c998c94da3dd2b5.svg} (100%) rename docs/images/chapters/bsplined/{15f9e6eea05599fe6a5eac609ca42cfa.svg => 6b7f07a5f80edaef59d07e86e9d9a668.svg} (100%) create mode 100644 docs/images/chapters/bsplines/5d3b04c3161a3429ce651bb7a5fa0399.png create mode 100644 docs/images/chapters/bsplines/610232b8f7ce7ef3f0f012d55e385c6d.png create mode 100644 docs/images/chapters/bsplines/8caa3e8ff614ad9731b15dacaba98c3c.png create mode 100644 docs/images/chapters/bsplines/93146ea89bb21999d9e18b57dd1bdd29.png create mode 100644 docs/images/chapters/bsplines/fc654445500dd595d6ae9de27a3dc46c.png delete mode 100644 docs/images/chapters/circles_cubic/3c6f863c77cc2100573bf71adaabc12e.png create mode 100644 docs/images/chapters/circles_cubic/8676cce0d4394ae095c7e50be1238aa0.png rename docs/images/chapters/curveintersection/{eae3bb142567d9e2b8c1e4d42e8ef505.png => 914e097fe4341697e05b6fd328cc4c91.png} (100%) create mode 100644 docs/js/custom-element/api/types/bspline.js create mode 100644 docs/js/custom-element/api/util/spline.js diff --git a/docs/chapters/arcapproximation/arc.js b/docs/chapters/arcapproximation/arc.js new file mode 100644 index 00000000..66d19e94 --- /dev/null +++ b/docs/chapters/arcapproximation/arc.js @@ -0,0 +1,101 @@ +// setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} + +let curve, utils = Bezier.getUtils(); + +setup() { + curve = Bezier.defaultCubic(this); + setMovable(curve.points); + setSlider(`.slide-control`, `error`, 0.5); +} + +draw() { + clear(); + + curve.drawSkeleton(); + curve.drawCurve(); + + setColor(`#FF000040`); + let a = this.getArc(curve); + arc( + a.x, a.y, a.r, a.s, a.e, + // draw a wedge, not just the arc + a.x, a.y + ); + + setColor("black"); + text(`Arc approximation with total error ${this.error}`, this.width/2, 15, CENTER); + curve.drawPoints(); +} + +getArc(curve) { + let ts = 0, + te = 1, + tm = te, + safety = 0, + np1 = curve.get(ts), np2, np3, + arc, + currGood = false, + prevGood = false, + done, + prev_e = 1, + step = 0; + + // Find where the good/bad boundary is + te = 1; + + // step 2: find the best possible arc + do { + prevGood = currGood; + tm = (ts + te) / 2; + step++; + + np2 = curve.get(tm); + np3 = curve.get(te); + + arc = utils.getccenter(np1, np2, np3); + arc.interval = { start: ts, end: te, }; + + let error = this.computeError(arc, np1, ts, te); + currGood = (error <= this.error); + + done = prevGood && !currGood; + if (!done) prev_e = te; + + // this arc is fine: try a wider arc + if (currGood) { + // if e is already at max, then we're done for this arc. + if (te >= 1) { + // make sure we cap at t=1 + arc.interval.end = prev_e = 1; + // if we capped the arc segment to t=1 we also need to make sure that + // the arc's end angle is correct with respect to the bezier end point. + if (te > 1) { + let d = { + x: arc.x + arc.r * cos(arc.e), + y: arc.y + arc.r * sin(arc.e), + }; + arc.e += utils.angle({ x: arc.x, y: arc.y }, d, curve.points[3]); + } + done = true; + break; + } + // if not, move it up by half the iteration distance + te = te + (te - ts) / 2; + } + + // This is a bad arc: we need to move 'e' down to find a good arc + else { te = tm; } + } while (!done && safety++ < 100); + + return arc; +} + +computeError(pc, np1, s, e) { + const q = (e - s) / 4, + c1 = curve.get(s + q), + c2 = curve.get(e - q), + ref = dist(pc.x, pc.y, np1.x, np1.y), + d1 = dist(pc.x, pc.y, c1.x, c1.y), + d2 = dist(pc.x, pc.y, c2.x, c2.y); + return abs(d1 - ref) + abs(d2 - ref); +} diff --git a/docs/chapters/arcapproximation/arcs.js b/docs/chapters/arcapproximation/arcs.js new file mode 100644 index 00000000..607ecf8f --- /dev/null +++ b/docs/chapters/arcapproximation/arcs.js @@ -0,0 +1,30 @@ +// setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} + +let curve, utils = Bezier.getUtils(); + +setup() { + curve = Bezier.defaultCubic(this); + setMovable(curve.points); + setSlider(`.slide-control`, `error`, 0.5); +} + +draw() { + clear(); + + curve.drawSkeleton(); + curve.drawCurve(); + + // See "arc.js" for the code required to find arcs on the curve. + let arcs = curve.arcs(this.error); + arcs.forEach(a => { + setColor( randomColor(0.3) ); + arc( + a.x, a.y, a.r, a.s, a.e, + a.x, a.y + ); + }); + + setColor("black"); + text(`Arc approximation with total error ${this.error}`, this.width/2, 15, CENTER); + curve.drawPoints(); +} diff --git a/docs/chapters/arcapproximation/content.en-GB.md b/docs/chapters/arcapproximation/content.en-GB.md index ffadbae0..1019c1e9 100644 --- a/docs/chapters/arcapproximation/content.en-GB.md +++ b/docs/chapters/arcapproximation/content.en-GB.md @@ -6,44 +6,40 @@ We already saw in the section on circle approximation that this will never yield The approach is fairly simple: pick a starting point on the curve, and pick two points that are further along the curve. Determine the circle that goes through those three points, and see if it fits the part of the curve we're trying to approximate. Decent fit? Try spacing the points further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've found the "good approximation/bad approximation" boundary, record the "good" arc, and then move the starting point up to overlap the end point we previously found. Rinse and repeat until we've covered the entire curve. -So: step 1, how do we find a circle through three points? That part is actually really simple. You may remember (if you ever learned it!) that a line between two points on a circle is called a [chord](https://en.wikipedia.org/wiki/Chord_%28geometry%29), and 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. +We already saw how to fit a circle through three points in the section on [creating a curve from three points](#pointcurves), and finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, to the end point, over the remaining point. -So: if we have have three points, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle. So we find the centers of the chords, find the perpendicular lines, find the intersection of those lines, and thus find the center of the circle. +So, how can we convert a Bézier curve into a (sequence of) circular arc(s)? -The following graphic shows this procedure with a different colour for each chord and its associated perpendicular through the center. You can move the points around as much as you like, those lines will always meet! - - - -So, with the procedure on how to find a circle through three points, finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, over the remaining point, to the end point. - -So how can we convert a Bézier curve into a (sequence of) circular arc(s)? - -- Start at t=0 -- Pick two points further down the curve at some value m = t + n and e = t + 2n +- Start at `t=0` +- Pick two points further down the curve at some value `m = t + n` and `e = t + 2n` - Find the arc that these points define - Determine how close the found arc is to the curve: - - Pick two additional points e1 = t + n/2 and e2 = t + n + n/2. + - Pick two additional points `e1 = t + n/2` and `e2 = t + n + n/2`. - These points, if the arc is a good approximation of the curve interval chosen, should - lie on the circle, so their distance to the center of the circle should be the + lie `on` the circle, so their distance to the center of the circle should be the same as the distance from any of the three other points to the center. - For point points, determine the (absolute) error between the radius of the circle, and the - actual distance from the center of the circle to the point on the curve. + `actual` distance from the center of the circle to the point on the curve. - If this error is too high, we consider the arc bad, and try a smaller interval. -The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a [Binary Search](https://en.wikipedia.org/wiki/Binary_search_algorithm). +The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at `t=0.5`, and then we start performing a [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm). -1. We start with {0, 0.5, 1} -2. That'll fail, so we retry with the interval halved: {0, 0.25, 0.5} - - If that arc's good, we move back up by half distance: {0, 0.375, 0.75}. - - However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}. -3. We keep doing this over and over until we have two arcs found in sequence of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the former. +1. We start with `low=0`, `mid=0.5` and `high=1` +2. That'll fail, so we retry with the interval halved: `{0, 0.25, 0.5}` + - If that arc's good, we move back up by half distance: `{0, 0.375, 0.75}`. + - However, if the arc was still bad, we move _down_ by half the distance: `{0, 0.125, 0.25}`. +3. We keep doing this over and over until we have two arcs, in sequence, of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the good arc. The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a combined half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is. - + + + With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined arc's starting point, and using points further down the curve. We keep trying this until the found end point is for t=1, at which point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve: - + + + So... what is this good for? Obviously, if you're working with technologies that can't do curves, but can do lines and circles, then the answer is pretty straightforward, but what else? There are some reasons why you might need this technique: using circular arcs means you can determine whether a coordinate lies "on" your curve really easily (simply compute the distance to each circular arc center, and if any of those are close to the arc radii, at an angle between the arc start and end, bingo, this point can be treated as lying "on the curve"). Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which, based on your application! diff --git a/docs/chapters/arcapproximation/handler.js b/docs/chapters/arcapproximation/handler.js deleted file mode 100644 index 084fbd64..00000000 --- a/docs/chapters/arcapproximation/handler.js +++ /dev/null @@ -1,184 +0,0 @@ -var atan2 = Math.atan2, PI = Math.PI, TAU = 2*PI, cos = Math.cos, sin = Math.sin; - -module.exports = { - // These are functions that can be called "From the page", - // rather than being internal to the sketch. This is useful - // for making on-page controls hook into the sketch code. - statics: { - keyHandlingOptions: { - propName: "error", - values: { - "38": 0.1, // up arrow - "40": -0.1 // down arrow - }, - controller: function(api) { - if (api.error < 0.1) { - api.error = 0.1; - } - } - } - }, - - /** - * Setup up a skeleton curve that, when using its - * points for a B-spline, can form a circle. - */ - setupCircle: function(api) { - var curve = new api.Bezier(70,70, 140,40, 240,130); - api.setCurve(curve); - }, - - /** - * Set up the default quadratic curve. - */ - setupQuadratic: function(api) { - var curve = api.getDefaultQuadratic(); - api.setCurve(curve); - }, - - /** - * Set up the default cubic curve. - */ - setupCubic: function(api) { - var curve = api.getDefaultCubic(); - api.setCurve(curve); - api.error = 0.5; - }, - - /** - * Given three points, find the (only!) circle - * that passes through all three points, based - * on the fact that the perpendiculars of the - * chords between the points all cross each - * other at the center of that circle. - */ - getCCenter: function(api, p1, p2, p3) { - // deltas - var dx1 = (p2.x - p1.x), - dy1 = (p2.y - p1.y), - dx2 = (p3.x - p2.x), - dy2 = (p3.y - p2.y); - - // perpendiculars (quarter circle turned) - var dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2), - dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2), - dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2), - dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2); - - // chord midpoints - var mx1 = (p1.x + p2.x)/2, - my1 = (p1.y + p2.y)/2, - mx2 = (p2.x + p3.x)/2, - my2 = (p2.y + p3.y)/2; - - // midpoint offsets - var mx1n = mx1 + dx1p, - my1n = my1 + dy1p, - mx2n = mx2 + dx2p, - my2n = my2 + dy2p; - - // intersection of these lines: - var i = api.utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n); - var r = api.utils.dist(i,p1); - - // arc start/end values, over mid point - var s = atan2(p1.y - i.y, p1.x - i.x), - m = atan2(p2.y - i.y, p2.x - i.x), - e = atan2(p3.y - i.y, p3.x - i.x); - - // determine arc direction (cw/ccw correction) - var __; - if (sm || m>e) { s += TAU; } - if (s>e) { __=e; e=s; s=__; } - } else { - if (e api.drawCircle(p,3)); - - // chords and perpendicular lines - var m; - - api.setColor("blue"); - api.drawLine(pts[0], pts[1]); - m = {x: (pts[0].x + pts[1].x)/2, y: (pts[0].y + pts[1].y)/2}; - api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)}); - - api.setColor("red"); - api.drawLine(pts[1], pts[2]); - m = {x: (pts[1].x + pts[2].x)/2, y: (pts[1].y + pts[2].y)/2}; - api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)}); - - api.setColor("green"); - api.drawLine(pts[2], pts[0]); - m = {x: (pts[2].x + pts[0].x)/2, y: (pts[2].y + pts[0].y)/2}; - api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)}); - - // center - api.setColor("black"); - api.drawPoint(C); - api.setFill("black"); - api.text("Intersection point", C, {x:-25, y:10}); - }, - - /** - * Draw a single arc being fit to a Bezier curve, - * to show off the general application. - */ - drawSingleArc: function(api, curve) { - api.reset(); - var arcs = curve.arcs(api.error); - api.drawSkeleton(curve); - api.drawCurve(curve); - - var a = arcs[0]; - api.setColor("red"); - api.setFill("rgba(255,0,0,0.2)"); - api.debug = true; - api.drawArc(a); - - api.setFill("black"); - api.text("Arc approximation with total error " + api.utils.round(api.error,1), {x:10, y:15}); - }, - - /** - * Draw an arc approximation for an entire Bezier curve. - */ - drawArcs: function(api, curve) { - api.reset(); - var arcs = curve.arcs(api.error); - api.drawSkeleton(curve); - api.drawCurve(curve); - arcs.forEach(a => { - api.setRandomColor(0.3); - api.setFill(api.getColor()); - api.drawArc(a); - }); - - api.setFill("black"); - api.text("Arc approximation with total error " + api.utils.round(api.error,1) + " per arc segment", {x:10, y:15}); - } -}; diff --git a/docs/chapters/bsplined/content.en-GB.md b/docs/chapters/bsplined/content.en-GB.md index 967ff0de..db104be2 100644 --- a/docs/chapters/bsplined/content.en-GB.md +++ b/docs/chapters/bsplined/content.en-GB.md @@ -24,41 +24,47 @@ So, much as for Bézier derivatives, we see a derivative function that is simply As a concrete example, let's look at cubic (=degree 3) B-Spline with five coordinates, and with uniform knot vector of length 3 + 5 + 1 = 9: -\[\begin{array}{l} - d = 3, \\ - P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\ - knots = {0,1,2,3,4,5,6,7,8} -\end{array}\] +\[ + \begin{array}{l} + d = 3, \\ + P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\ + knots = {0,1,2,3,4,5,6,7,8} + \end{array} +\] Applying the above knowledge, we end up with a new B-Spline of degree d-1, with four points P': -\[\begin{array}{l} - P_0 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) - = \frac{3}{knot_{4} - knot_{1}} (P_1 - P_0) - = \frac{3}{3} (P_1 - P_0) - = (135, -210) \\ - P_1 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) - = \frac{3}{knot_{5} - knot_{2}} (P_2 - P_1) - = \frac{3}{3} (P_2 - P_1) - = (135, 105) \\ - P_2 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) - = \frac{3}{knot_{6} - knot_{3}} (P_3 - P_2) - = \frac{3}{3} (P_3 - P_2) - = (135, -110) \\ - P_3 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) - = \frac{3}{knot_{7} - knot_{4}} (P_4 - P_3) - = \frac{3}{3} (P_4 - P_3) - = (105, 230) \\ -\end{array}\] +\[ + \begin{array}{l} + P_0 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) + = \frac{3}{knot_{4} - knot_{1}} (P_1 - P_0) + = \frac{3}{3} (P_1 - P_0) + = (135, -210) \\ + P_1 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) + = \frac{3}{knot_{5} - knot_{2}} (P_2 - P_1) + = \frac{3}{3} (P_2 - P_1) + = (135, 105) \\ + P_2 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) + = \frac{3}{knot_{6} - knot_{3}} (P_3 - P_2) + = \frac{3}{3} (P_3 - P_2) + = (135, -110) \\ + P_3 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i) + = \frac{3}{knot_{7} - knot_{4}} (P_4 - P_3) + = \frac{3}{3} (P_4 - P_3) + = (105, 230) \\ + \end{array} +\] So, we end up with a derivative that has as parameters: -\[\begin{array}{l} - d = 3, \\ - P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\ - knots = {0,1,2,3,4,5,6,7,8} -\end{array}\] +\[ + \begin{array}{l} + d = 3, \\ + P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\ + knots = {0,1,2,3,4,5,6,7,8} + \end{array} +\] diff --git a/docs/chapters/bsplines/basic-sketch.js b/docs/chapters/bsplines/basic-sketch.js deleted file mode 100644 index 9a3e97af..00000000 --- a/docs/chapters/bsplines/basic-sketch.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - degree: 3, - activeDistance: 9, - - setup() { - this.size(600, 300); - this.draw(); - }, - - draw() { - this.clear(); - this.grid(25); - var p = this.points[0]; - this.points.forEach(n => { - this.stroke(200); - this.line(n.x, n.y, p.x, p.y); - p = n; - this.stroke(0); - this.circle(p.x, p.y, 4); - }); - this.drawSplineData(); - }, - - drawSplineData() { - if (this.points.length <= this.degree) return; - var mapped = this.points.map(p => [p.x, p.y]); - this.drawCurve(mapped); - this.drawKnots(mapped); - } -}; diff --git a/docs/chapters/bsplines/basic.js b/docs/chapters/bsplines/basic.js new file mode 100644 index 00000000..8251fc19 --- /dev/null +++ b/docs/chapters/bsplines/basic.js @@ -0,0 +1,57 @@ +let points=[]; + +setup() { + points = [ + {x:25, y:160}, + {x:90, y:75}, + {x:190,y:245}, + {x:290,y:25}, + {x:400,y:255}, + {x:480,y:70}, + {x:560,y:170} + ]; + setMovable(points); +} + +draw() { + clear(); + + setStroke(`lightgrey`); + drawGrid(20); + + setStroke(`#CC00CC99`); + for (let i=0, e=points.length-1, p, n; i circle(p.x, p.y, 3)); + + this.drawSplineData(); +} + +drawSplineData() { + // we'll need at least 4 points + if (points.length <= 3) return; + + let spline = new BSpline(this, points); + + noFill(); + setStroke(`black`); + start(); + spline.getLUT((points.length - 3) * 20).forEach(p => vertex(p.x, p.y)); + end(); +} + +onMouseDown() { + if (!this.currentPoint) { + points.push({ + x: this.cursor.x, + y: this.cursor.y + }); + resetMovable(points); + redraw(); + } +} diff --git a/docs/chapters/bsplines/center-cut-bspline.js b/docs/chapters/bsplines/center-cut-bspline.js deleted file mode 100644 index bc3a6f4a..00000000 --- a/docs/chapters/bsplines/center-cut-bspline.js +++ /dev/null @@ -1,52 +0,0 @@ -module.exports = { - degree: 3, - activeDistance: 9, - - setup() { - this.size(400, 400); - - var TAU = Math.PI*2; - for (let i=0; i { - this.stroke(200); - this.line(n.x, n.y, p.x, p.y); - p = n; - this.stroke(0); - this.circle(p.x, p.y, 4); - }); - this.drawSplineData(); - }, - - drawSplineData() { - if (this.points.length <= this.degree) return; - var mapped = this.points.map(p => [p.x, p.y]); - this.drawCurve(mapped); - this.drawKnots(mapped); - } -}; diff --git a/docs/chapters/bsplines/content.en-GB.md b/docs/chapters/bsplines/content.en-GB.md index 0bf1fd2e..5502fc08 100644 --- a/docs/chapters/bsplines/content.en-GB.md +++ b/docs/chapters/bsplines/content.en-GB.md @@ -4,9 +4,9 @@ No discussion on Bézier curves is complete without also giving mention of that First off: B-Splines are [piecewise polynomial interpolation curves](https://en.wikipedia.org/wiki/Piecewise), where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections. -What do they look like? They look like this! .. okay that's an empty graph, but simply click to place some point, with the stipulation that you need at least four point to see any curve. More than four points simply draws a longer B-Spline curve: +What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the spline curve drawn. - + The important part to notice here is that we are **not** doing the same thing with B-Splines that we do for poly-Béziers or Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that follows, so we can only have 4, 7, 10, 13, 16, etc point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth interpolations of *each possible curve involving four consecutive points*, such that at any point along the curve except for our start and end points, our on-curve coordinate is defined by four control points. @@ -17,6 +17,7 @@ Consider the difference to be this: In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work. + ## How to compute a B-Spline curve: some maths Given a B-Spline of degree `d` and thus order `k=d+1` (so a quadratic B-Spline is degree 2 and order 3, a cubic B-Spline is degree 3 and order 4, etc) and `n` control points `P0` through `Pn-1`, we can compute a point on the curve for some value `t` in the interval [0,1] (where 0 is the start of the curve, and 1 the end, just like for Bézier curves), by evaluating the following function: @@ -47,6 +48,7 @@ So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for And this function finally has a straight up evaluation: if a `t` value lies within a knot-specific interval once we reach a `k=1` value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale our `t` value first, so that it lies in the interval bounded by `knots[d]` and `knots[n]`, which are the start point and end point where curvature is controlled by exactly `order` control points. For instance, for degree 3 (=order 4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map `t` from [the interval 0,1] to the interval [4,8], and then use that value in the functions above, instead. + ## Can we simplify that? We can, yes. @@ -98,10 +100,13 @@ One thing we need to keep in mind is that we're working with a spline that is co If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate the terminating d() values first, then compute the a() constants, perform our multiplications, generate the previous step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers we start with and simply update them as we move up the triangle. So, let's implement that! + ## Cool, cool... but I don't know what to do with that information I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by one function each) influences the total curvature, given our knot values. And, because exploration is the key to discovery, let's make the knot vector a thing we can actually manipulate. Normally a proper knot vector has a constraint that any value is strictly equal to, or larger than the previous ones, but screw it this is programming, let's ignore that hard restriction and just mess with the knots however we like. + +
this.bindKnots(owner, knots, "interpolation-graph")}/> @@ -111,6 +116,7 @@ Changing the values in the knot vector changes how much each point influences th After reading the rest of this section you may want to come back here to try some specific knot vectors, and see if the resulting interpolation landscape makes sense given what you will now think should happen! + ## Running the computation Unlike the de Casteljau algorithm, where the `t` value stays the same at every iteration, for B-Splines that is not the case, and so we end having to (for each point we evaluate) run a fairly involving bit of recursive computation. The algorithm is discussed on [this Michigan Tech](http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/de-Boor.html) page, but an easier to read version is implemented by [b-spline.js](https://github.com/thibauts/b-spline/blob/master/index.js#L59-L71), so we'll look at its code. @@ -140,12 +146,14 @@ for(let L = 1; L <= order; L++) { (A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at `i=s` at each level of the interpolation, and we stop when `i = s - order + level`, so we always end up with a value for `i` such that those `v[i-1]` don't try to use an array index that doesn't exist) + ## Open vs. closed paths Much like poly-Béziers, B-Splines can be either open, running from the first point to the last point, or closed, where the first and last point are *the same point*. However, because B-Splines are an interpolation of curves, not just point, we can't simply make the first and last point the same, we need to link a few point point: for an order `d` B-Spline, we need to make the last `d` point the same as the first `d` points. And the easiest way to do this is to simply append `points.splice(0,d)` to `points`. Done! Of course if we want to manipulate these kind of curves we need to make sure to mark them as "closed" so that we know the coordinate for `points[0]` and `points[n-k]` etc. are the same coordinate, and manipulating one will equally manipulate the other, but programming generally makes this really easy by storing references to coordinates (or other linked values such as coordinate weights, discussed in the NURBS section) rather than separate coordinate objects. + ## Manipulating the curve through the knot vector The most important thing to understand when it comes to B-Splines is that they work *because* of the concept of a knot vector. As mentioned above, knots represent "where individual control points start/stop influencing the curve", but we never looked at the *values* that go in the knot vector. If you look back at the N() and a() functions, you see that interpolations are based on intervals in the knot vector, rather than the actual values in the knot vector, and we can exploit this to do some pretty interesting things with clever manipulation of the knot vector. Specifically there are four things we can do that are worth looking at: @@ -155,27 +163,28 @@ The most important thing to understand when it comes to B-Splines is that they w 3. we can collapse sequential knots to the same value, locally lowering curve complexity using "null" intervals, and 4. we can form a special case non-uniform vector, by combining (1) and (3) to for a vector with collapsed start and end knots, with a uniform vector in between. + ### Uniform B-Splines The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines *the same intervals*, or even [0,2,3,...,18,20,22], which also defines *the same intervals*, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to the curvature. -
- - this.bindKnots(owner, knots, "uniform-spline")}/> -
+ + + This is an important point: the intervals that the knot vector defines are *relative* intervals, so it doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve. The problem with uniform knot vectors is that, as we need `order` control points before we have any curve with which we can perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get rid of these, by being clever about how we apply the following uniformity-breaking approach instead... + ### Reducing local curve complexity by collapsing intervals -By collapsing knot intervals by making two or more consecutive knots have the same value, we can reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing `order` knots creates a situation where all continuity is lost and the curve "kinks". +Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing `order` knots creates a situation where all continuity is lost and the curve "kinks". + + + + -
- - this.bindKnots(owner, knots, "center-cut-bspline")}/> -
### Open-Uniform B-Splines @@ -183,31 +192,24 @@ By combining knot interval collapsing at the start and end of the curve, with un For any curve of degree `D` with control points `N`, we can define a knot vector of length `N+D+1` in which the values `0 ... D+1` are the same, the values `D+1 ... N+1` follow the "uniform" pattern, and the values `N+1 ... N+D+1` are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape. -
- - this.bindKnots(owner, knots, "open-uniform-bspline")}/> -
+ + + + ### Non-uniform B-Splines -This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, and that is that any value `knots[k+1]` should be equal to, or greater than `knots[k]`. +This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, other than that any value `knots[k+1]` should be greater than or equal to `knots[k]`. ## One last thing: Rational B-Splines While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a control point carries, the close to that point the spline curve will lie, a bit like turning up the gravity of a control point. -
- { - // - } - - { - // this.bindKnots(owner, knots, "rational-uniform-bspline"); - this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights"); - }} /> -
+ + + -Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS are an important type of curve in computer-facilitated design, used a lot in 3D modelling (as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers. +Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the [NURBS](https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline), or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers. While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the last. diff --git a/docs/chapters/bsplines/handler.js b/docs/chapters/bsplines/handler.js deleted file mode 100644 index a8a0a796..00000000 --- a/docs/chapters/bsplines/handler.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - basicSketch: require('./basic-sketch'), - interpolationGraph: require('./interpolation-graph'), - uniformBSpline: require('./uniform-bspline'), - centerCutBSpline: require('./center-cut-bspline'), - openUniformBSpline: require('./open-uniform-bspline'), - rationalUniformBSpline: require('./rational-uniform-bspline'), - - bindKnots: function(owner, knots, ref) { - this.refs[ref].bindKnots(owner, knots); - }, - - bindWeights: function(owner, weights, closed, ref) { - this.refs[ref].bindWeights(owner, weights, closed); - } -}; diff --git a/docs/chapters/bsplines/open-uniform-bspline.js b/docs/chapters/bsplines/open-uniform-bspline.js deleted file mode 100644 index d60be4bd..00000000 --- a/docs/chapters/bsplines/open-uniform-bspline.js +++ /dev/null @@ -1,45 +0,0 @@ -module.exports = { - degree: 3, - activeDistance: 9, - - setup() { - this.size(400, 400); - - var TAU = Math.PI*2; - for (let i=0; i { - this.stroke(200); - this.line(n.x, n.y, p.x, p.y); - p = n; - this.stroke(0); - this.circle(p.x, p.y, 4); - }); - this.drawSplineData(); - }, - - drawSplineData() { - if (this.points.length <= this.degree) return; - var mapped = this.points.map(p => [p.x, p.y]); - this.drawCurve(mapped); - this.drawKnots(mapped); - } -}; diff --git a/docs/chapters/bsplines/rational-uniform-bspline.js b/docs/chapters/bsplines/rational-uniform-bspline.js deleted file mode 100644 index f3d823aa..00000000 --- a/docs/chapters/bsplines/rational-uniform-bspline.js +++ /dev/null @@ -1,50 +0,0 @@ -module.exports = { - degree: 3, - activeDistance: 9, - weights: [], - - setup() { - this.size(400, 400); - - var TAU = Math.PI*2; - var r = this.width/3; - for (let i=0; i<6; i++) { - this.points.push({ - x: this.width/2 + r * Math.cos(i/6 * TAU), - y: this.height/2 + r * Math.sin(i/6 * TAU) - }); - } - this.points = this.points.concat(this.points.slice(0,3)); - this.closed = this.degree; - - this.knots = this.formKnots(this.points); - this.weights = this.formWeights(this.points); - - if(this.props.controller) { - this.props.controller(this, this.knots, this.weights, this.closed); - } - - this.draw(); - }, - - draw() { - this.clear(); - this.grid(25); - var p = this.points[0]; - this.points.forEach(n => { - this.stroke(200); - this.line(n.x, n.y, p.x, p.y); - p = n; - this.stroke(0); - this.circle(p.x, p.y, 4); - }); - this.drawSplineData(); - }, - - drawSplineData() { - if (this.points.length <= this.degree) return; - var mapped = this.points.map(p => [p.x, p.y]); - this.drawCurve(mapped); - this.drawKnots(mapped); - } -}; diff --git a/docs/chapters/bsplines/rational-uniform.js b/docs/chapters/bsplines/rational-uniform.js new file mode 100644 index 00000000..5ff300ad --- /dev/null +++ b/docs/chapters/bsplines/rational-uniform.js @@ -0,0 +1,43 @@ +let points=[]; + +setup() { + var r = this.width/3; + for (let i=0; i<6; i++) { + points.push({ + x: this.width/2 + r * Math.cos(i/6 * TAU), + y: this.height/2 + r * Math.sin(i/6 * TAU) + }); + } + points = points.concat(points.slice(0,3)); + setMovable(points); +} + +draw() { + clear(); + + setStroke(`lightgrey`); + drawGrid(20); + + setStroke(`#CC00CC99`); + for (let i=0, e=points.length-1, p, n; i circle(p.x, p.y, 3)); + + this.drawSplineData(); +} + +drawSplineData() { + const spline = new BSpline(this, points, !!this.parameters.open); + spline.formWeights(); + + noFill(); + setStroke(`black`); + start(); + spline.getLUT((points.length - 3) * 20).forEach(p => vertex(p.x, p.y)); + end(); +} diff --git a/docs/chapters/bsplines/reduced.js b/docs/chapters/bsplines/reduced.js new file mode 100644 index 00000000..51566015 --- /dev/null +++ b/docs/chapters/bsplines/reduced.js @@ -0,0 +1,49 @@ +let points=[], knots; + +setup() { + for (let s=TAU/9, i=s/2; i circle(p.x, p.y, 3)); + + this.drawSplineData(); +} + +drawSplineData() { + const spline = new BSpline(this, points, !!this.parameters.open); + + const knots = spline.formKnots(); + const m = round(points.length/2)|0; + knots[m+0] = knots[m]; + knots[m+1] = knots[m]; + knots[m+2] = knots[m]; + for (let i=m+3; i vertex(p.x, p.y)); + end(); +} diff --git a/docs/chapters/bsplines/uniform-bspline.js b/docs/chapters/bsplines/uniform-bspline.js deleted file mode 100644 index 7da2a7ed..00000000 --- a/docs/chapters/bsplines/uniform-bspline.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - degree: 3, - activeDistance: 9, - - setup() { - this.size(400, 400); - - var TAU = Math.PI*2; - for (let i=0; i { - this.stroke(200); - this.line(n.x, n.y, p.x, p.y); - p = n; - this.stroke(0); - this.circle(p.x, p.y, 4); - }); - this.drawSplineData(); - }, - - drawSplineData() { - if (this.points.length <= this.degree) return; - var mapped = this.points.map(p => [p.x, p.y]); - this.drawCurve(mapped); - this.drawKnots(mapped); - } -}; diff --git a/docs/chapters/bsplines/uniform.js b/docs/chapters/bsplines/uniform.js new file mode 100644 index 00000000..b96579e7 --- /dev/null +++ b/docs/chapters/bsplines/uniform.js @@ -0,0 +1,41 @@ +let points=[]; + +setup() { + for (let s=TAU/9, i=s/2; i circle(p.x, p.y, 3)); + + this.drawSplineData(); +} + +drawSplineData() { + const spline = new BSpline(this, points); + spline.formKnots(!!this.parameters.open); + + noFill(); + setStroke(`black`); + start(); + spline.getLUT((points.length - 3) * 20).forEach(p => vertex(p.x, p.y)); + end(); +} diff --git a/docs/chapters/circles_cubic/circle.js b/docs/chapters/circles_cubic/circle.js index 3a35ad85..32dc4ce3 100644 --- a/docs/chapters/circles_cubic/circle.js +++ b/docs/chapters/circles_cubic/circle.js @@ -1,7 +1,7 @@ let curve, r; setup() { - r = (this.width/4) | 0; + r = 100; curve = new Bezier(this, [ { x: r, y: 0 }, { x: r, y: 0.55228 * r }, diff --git a/docs/chapters/circles_cubic/content.en-GB.md b/docs/chapters/circles_cubic/content.en-GB.md index 64f77689..4d6af720 100644 --- a/docs/chapters/circles_cubic/content.en-GB.md +++ b/docs/chapters/circles_cubic/content.en-GB.md @@ -179,4 +179,4 @@ Which, in decimal values, rounded to six significant digits, is: Of course, this is for a circle with radius 1, so if you have a different radius circle, simply multiply the coordinate by the radius you need. And then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin: - + diff --git a/docs/chapters/curveintersection/curve-curve.js b/docs/chapters/curveintersection/curve-curve.js index 6acf0a7c..ccb1184c 100644 --- a/docs/chapters/curveintersection/curve-curve.js +++ b/docs/chapters/curveintersection/curve-curve.js @@ -4,7 +4,7 @@ setup() { setPanelCount(3); this.pairReset(); this.setupEventListening(); - setSlider(`.slide-control`, `epsilon`, 1.0, v => this.reset()); + setSlider(`.slide-control`, `epsilon`, 1.0, v => this.reset(v)); } pairReset() { @@ -15,11 +15,12 @@ pairReset() { this.reset(); } -reset() { +reset(v) { if (next && next.disabled) next.disabled = false; this.pairs = [[curve1, curve2]]; this.finals = []; this.step = 0; + return v; } setupEventListening() { diff --git a/docs/images/chapters/arcapproximation/6f30b487d0cb60a4caeed4a199c48253.png b/docs/images/chapters/arcapproximation/6f30b487d0cb60a4caeed4a199c48253.png new file mode 100644 index 0000000000000000000000000000000000000000..2d06daa7ff52138d708dabf0269ac6601e799c1b GIT binary patch literal 14689 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 - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - + + - - + + - - - - - + + - - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/chapters/bsplines/4c8f9814c50c708757eeb5a68afabb7f.svg b/docs/images/chapters/bsplines/4c8f9814c50c708757eeb5a68afabb7f.svg index 354cd5b9..dd56e5d2 100644 --- a/docs/images/chapters/bsplines/4c8f9814c50c708757eeb5a68afabb7f.svg +++ b/docs/images/chapters/bsplines/4c8f9814c50c708757eeb5a68afabb7f.svg @@ -1,185 +1,187 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - + + - - - - - - - - + + - - + + - - - - - + + - - + + - - + + + + + - - + + - - + + + + + + + + - - - - - - - - + + - - + + - - + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/chapters/bsplines/5d3b04c3161a3429ce651bb7a5fa0399.png b/docs/images/chapters/bsplines/5d3b04c3161a3429ce651bb7a5fa0399.png new file mode 100644 index 0000000000000000000000000000000000000000..a4778691aceddf7a7938b0985498ca33c9b04fe6 GIT binary patch literal 12786 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 Zd&%WSN8dZ*2mXlzqOPokC{wfu{crc#p+^7! literal 0 HcmV?d00001 diff --git a/docs/images/chapters/bsplines/610232b8f7ce7ef3f0f012d55e385c6d.png b/docs/images/chapters/bsplines/610232b8f7ce7ef3f0f012d55e385c6d.png new file mode 100644 index 0000000000000000000000000000000000000000..8c1e929f37ad4282ff04fd2be848444fdd7f05c9 GIT binary patch literal 20215 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 literal 0 HcmV?d00001 diff --git a/docs/images/chapters/bsplines/763838ea6f9e6c6aa63ea5f9c6d9542f.svg b/docs/images/chapters/bsplines/763838ea6f9e6c6aa63ea5f9c6d9542f.svg index 7cbd2dcb..12d10fc9 100644 --- a/docs/images/chapters/bsplines/763838ea6f9e6c6aa63ea5f9c6d9542f.svg +++ b/docs/images/chapters/bsplines/763838ea6f9e6c6aa63ea5f9c6d9542f.svg @@ -1,134 +1,140 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - - - - + + - - + + + + + + + + - - - - - + + - - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/chapters/bsplines/7962d6fea86da6f53a7269fba30f0138.svg b/docs/images/chapters/bsplines/7962d6fea86da6f53a7269fba30f0138.svg index 859bdeae..3019a4fd 100644 --- a/docs/images/chapters/bsplines/7962d6fea86da6f53a7269fba30f0138.svg +++ b/docs/images/chapters/bsplines/7962d6fea86da6f53a7269fba30f0138.svg @@ -1,535 +1,535 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/docs/images/chapters/bsplines/892209dad8fd1f839470dd061e870913.svg b/docs/images/chapters/bsplines/892209dad8fd1f839470dd061e870913.svg index 30df74d0..b2da3c53 100644 --- a/docs/images/chapters/bsplines/892209dad8fd1f839470dd061e870913.svg +++ b/docs/images/chapters/bsplines/892209dad8fd1f839470dd061e870913.svg @@ -1,135 +1,135 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - + + - - + + - - + + - - + + - - + + + + + diff --git a/docs/images/chapters/bsplines/8caa3e8ff614ad9731b15dacaba98c3c.png b/docs/images/chapters/bsplines/8caa3e8ff614ad9731b15dacaba98c3c.png new file mode 100644 index 0000000000000000000000000000000000000000..05f11b8d287bfd959ca0c2eca35f4e6d417e264b GIT binary patch literal 13926 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 literal 0 HcmV?d00001 diff --git a/docs/images/chapters/bsplines/93146ea89bb21999d9e18b57dd1bdd29.png b/docs/images/chapters/bsplines/93146ea89bb21999d9e18b57dd1bdd29.png new file mode 100644 index 0000000000000000000000000000000000000000..99dc2d9ac484da7112f3fcb12bcb333e84c99b6c GIT binary patch literal 12771 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 g|6ezK - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - + + - - + + + + + - - - - - + + - - + + + + + + + + - - + + - - + + - - + + - - - - + + - - + + - - + + - - - - + + - - + + - - + + - - + + + + + diff --git a/docs/images/chapters/bsplines/cf45d1ea00d4866abc8a058b130299b4.svg b/docs/images/chapters/bsplines/cf45d1ea00d4866abc8a058b130299b4.svg index 23a877f0..0725f717 100644 --- a/docs/images/chapters/bsplines/cf45d1ea00d4866abc8a058b130299b4.svg +++ b/docs/images/chapters/bsplines/cf45d1ea00d4866abc8a058b130299b4.svg @@ -1,307 +1,307 @@ - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + + + + + + + - - - - - + + - - + + - - + + + + + - - + + + + + - - + + - - + + + + + + + + + + + + + + - - - - - - - - + + - - - - - + + - - + + - - - - - + + - - + + - - + + + + + + + + + + + + + + - - + + - - + + - - - - - - - - - - - + + - - + + - - + + + + + + + + + + + + + + + + + - - + + - - + + - - - - + + diff --git a/docs/images/chapters/bsplines/fc654445500dd595d6ae9de27a3dc46c.png b/docs/images/chapters/bsplines/fc654445500dd595d6ae9de27a3dc46c.png new file mode 100644 index 0000000000000000000000000000000000000000..0c09c7dbe4db27ecab95c349fed907f231035cfd GIT binary patch literal 17198 zcmdtKbySscv@N;;=`KOK6hu;yZZ;tzAX3sHN+=}_(j`b4V9)}JG}4`dfP_eQ3DPMI zcYU67-niq9bH}~+&-aEn)a|#w{rzgKx#pZ}Ki5=OCMKXGK%r2?w^YztC=^C4@*58a zUh%IN?t(vXEN&~KQRm37)Y?z4P$(AEEwr4rN8;Lqr^mg4QQ=Jqg?^$ZKdB5xRRgZ$ zbA9#K$B~om=(Q+fQD)F2QzXYtZDZ#yz{K=wz^5XYN}V--dFk~nu6D^=7}z9MT(8Sy zUkTZ~ykfI`I`gR1Ep;N}(#Di)(u&tqQWB?zLD$5a+P?;D37rc~yz`5TSg7=mA01q6 zOADD+ir^>te2B7of%4um^}s(+`5C=$e)`iR6-|qWNV=N)U%<;q&Sh< z8?#_A^bU-&i zm@%LfRB4hEndfNUAL87$<|bibX*uze{!j!5kClVt0=v?ShK_}6=4J11$6R}(s`g$I zKb`Du)bgp5@12x3uR0xvpYyj4Y!)>HKAdkm$mgR+H-&IRXjMKxXdlb-cwfIrhz)knwS& z@d_ub%F4>j^Zg$mM9hUh+|}2|%NZbuY{e`!Ag(kb36Z~>pWJE@&}SX|UOI3@%*GxosPvGP2ZxD>CaFKLvYKgmPAU5IHxXCLK&Q8|00Rgx^Q3P(&G2_=rKui%L0$sda0vEp5QI zFHHfr#D0Q~*Pw*!v|-R%h-qV^;Dr)M4z1axU3G7CrP*~ zeNs>A&A!Lk($eBlBO|xxykq{>QudT{S;0x~FwP)qpM#aPwO~NRist`tE z6-{&T;w+p`QI{pMi~J@poaVmCrD>+RZ%i%?7qM5luKX?|y--ex^@WPs<(f-ud8`7* zR!`iu327V^vK#9{Y#iJd@*kEn1be;i|I{cxyU{)MB>f zx%Eh~RaC*X1G-X!D3e;RG`#|o%(m`Cu^?W!rwoYObMBf9D)0b8lid*5E|y%0XK z{Hm%1p(KnE#~bxioa@cKB>ij~wUVF3b67}rW>|R!=*|X%Ui&67neT_n(Jf5Wv;N(f zig}ds?Bd?aP+J?b-;2L%V{@A`&Bz>@_?^1KdXMIudfrx1!K-rLi1s^OF%@;3Mv*Xx z`p5Gc&L+7}LeODQieZY9^8V{l-nK;cx$1MOX~DiN^Sl&MEX#vxi=c&I?BxL4f*N0S zbrRT?A@D|vU!U))sHn(3e0Zho@kk)7RGRPUk;$LVgqzmAD9S5pvPw!g>WQKO7kLfL zR)>o!cDg0D)~h#Wmj`l8{@lKyg04Hf8tkCFJ%PiuaB6XCrf$9 z#>azkmN=yOV!wrJ$ZoN-BwPXF&GxW1cJOiQ)G?shfX)dAY*n55;JSO%Mha z&NPfZ*Ab&-lr26o4en(h(rsCAt1bwG;z3tim!m-v-fg0t_LY&x!ah4cM3#PKQ+|7K zDG277If2i(DG-~mx71EAW|yqJ%H>aBkMuE-UcOOdZ<@l^a!x@y9O;7|zq5Pxt_!0S zle`S-k(9qJe;cN#VT52%ppD#UT&}r>Gce)*+UCfDRM3?uMhStkF@pMH#P0eCC#wv!IVbr z9xhuP-mP1=hRdGN3)_!}cXg?`{P~Ifbg?_6&pNksi1-ObM6XgB*SM~}U4dL(7zPCg zgBR`#i5I=nxp{Anls=N*c+`!a`xfQ2J%20Lq?Yb>Jda#|)*ZWry`{m?a)+3Wx?_@+ z!Mx^z`m>0vJ0E6G_ow*3N>)qQFb_6WG~k*NO3&{R47 z4NI;!Es>qw;k6svmtaR~`lQhge%1YaTh&r@ICRb5qUg8Q%BRv{{IKo)4NVnZWd!WKpe~C{9Hb|rWx?oPaF#1K3JYf?w;!9~ zr@Ug;lPm?1=cA$Ry*0bBGG4c}(GYl<<>pOl%Wp3lqv%D(wG64) zX zCJg%uuuw5le`(cIq@xN81;$;6jP)xV(+s6|-z2>uyM-2Do-dh{ZA0OknSIs*?(0UYCGE#yuUiR!ZU9OWA54v!ib0`XrB#nkTKBG3cUaIVH!*jt?*;oXa=!B7-nVB z?QLubhiEv(TKDet6Tqht=x3s2V~QX%Vu$H_ zcC_30MJK25>7u&iPS*{x$U}`Qn$OW}$UlZl?PzohOrl=Drkel#n;@TNOY?$S^&Q7? zWT355eOM*QHqY^YdR(2aoex}oGF8V2fuw;wMlx`BcbATrnF&v(-mGBJQFOBrmwEbw zLtSHy=MJP5uFuI?}GZ68d zl5=*tF-X7o!L7=qT`o}oXljX~}$Ew=>jbq(lRi3FQoDtpd zFTt0w$0XiIlNNgMeIl&ve@Y(5fT1|Hyry@mEQO-I{s51SoqcOS&lG?1%3zA0Z?2vR zL`o*ldGg?p5F|=v4k>*x?C}{VYWBjiFo_9JG4j10R#0m?nByzuo5pCS=B*ZM%`rd{ zBZEW=<7&57c*r8(Q?HZ>%S1_jK0Xo&w^i8>!cTf(Ax_{V-Sny}PNJYFy`U&VGoTeV z&r1}GniY~TA2#zB&U?6GoM%)?(HutF_9?|TV#vfhYTs2;Oe>oTj&@7BV&vx1#~6#l z?FG1t+uMtw#5kfqU^P`>HCqb%P*%8?=8dF4oFYJJc=4W6OI*-#SE5ei%t(x3|GmYEHSR0h`#$# z3Bl?5Ump_FiP#%;GHRt-k5>pHTLF&!CFBr9L`LT9)r7ysdrSImrhN|fLn8{GtV&1R z8poPNck)QjKwCQ)_RZ3s*WVI~kiYviat-aLnXRo~ zWP$xŎ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 Hv%voWC|$TI literal 0 HcmV?d00001 diff --git a/docs/images/chapters/circles_cubic/3c6f863c77cc2100573bf71adaabc12e.png b/docs/images/chapters/circles_cubic/3c6f863c77cc2100573bf71adaabc12e.png deleted file mode 100644 index f536960f524a216740eb63ee521645b93b48f961..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12398 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 EKM}J^aR2}S literal 0 HcmV?d00001 diff --git a/docs/images/chapters/curveintersection/eae3bb142567d9e2b8c1e4d42e8ef505.png b/docs/images/chapters/curveintersection/914e097fe4341697e05b6fd328cc4c91.png similarity index 100% rename from docs/images/chapters/curveintersection/eae3bb142567d9e2b8c1e4d42e8ef505.png rename to docs/images/chapters/curveintersection/914e097fe4341697e05b6fd328cc4c91.png diff --git a/docs/index.html b/docs/index.html index 2c1e6963..619bdd6d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1588,7 +1588,7 @@ lli = function(line1, line2): Scripts are disabled. Showing fallback image. - + @@ -2265,10 +2265,10 @@ for p = 1 to points.length-3 (inclusive):

Which, in decimal values, rounded to six significant digits, is:

Of course, this is for a circle with radius 1, so if you have a different radius circle, simply multiply the coordinate by the radius you need. And then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin:

- + Scripts are disabled. Showing fallback image. - + @@ -2278,43 +2278,52 @@ for p = 1 to points.length-3 (inclusive):

Let's look at doing the exact opposite of the previous section: rather than approximating circular arc using Bézier curves, let's approximate Bézier curves using circular arcs.

We already saw in the section on circle approximation that this will never yield a perfect equivalent, but sometimes you need circular arcs, such as when you're working with fabrication machinery, or simple vector languages that understand lines and circles, but not much else.

The approach is fairly simple: pick a starting point on the curve, and pick two points that are further along the curve. Determine the circle that goes through those three points, and see if it fits the part of the curve we're trying to approximate. Decent fit? Try spacing the points further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've found the "good approximation/bad approximation" boundary, record the "good" arc, and then move the starting point up to overlap the end point we previously found. Rinse and repeat until we've covered the entire curve.

-

So: step 1, how do we find a circle through three points? That part is actually really simple. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and 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.

-

So: if we have have three points, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle. So we find the centers of the chords, find the perpendicular lines, find the intersection of those lines, and thus find the center of the circle.

-

The following graphic shows this procedure with a different colour for each chord and its associated perpendicular through the center. You can move the points around as much as you like, those lines will always meet!

- - -

So, with the procedure on how to find a circle through three points, finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, over the remaining point, to the end point.

-

So how can we convert a Bézier curve into a (sequence of) circular arc(s)?

+

We already saw how to fit a circle through three points in the section on creating a curve from three points, and finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, to the end point, over the remaining point.

+

So, how can we convert a Bézier curve into a (sequence of) circular arc(s)?

    -
  • Start at t=0
  • -
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • +
  • Start at t=0
  • +
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • Find the arc that these points define
  • Determine how close the found arc is to the curve:
      -
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • +
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • These points, if the arc is a good approximation of the curve interval chosen, should - lie on the circle, so their distance to the center of the circle should be the + lie on the circle, so their distance to the center of the circle should be the same as the distance from any of the three other points to the center.
    • For point points, determine the (absolute) error between the radius of the circle, and the -actual distance from the center of the circle to the point on the curve.
    • +actual distance from the center of the circle to the point on the curve.
    • If this error is too high, we consider the arc bad, and try a smaller interval.
-

The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a Binary Search.

+

The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a binary search.

    -
  1. We start with {0, 0.5, 1}
  2. -
  3. That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
      -
    • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
    • -
    • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • +
    • We start with low=0, mid=0.5 and high=1
    • +
    • That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
        +
      • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
      • +
      • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • -
    • We keep doing this over and over until we have two arcs found in sequence of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the former.
    • +
    • We keep doing this over and over until we have two arcs, in sequence, of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the good arc.

The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a combined half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is.

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

With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined arc's starting point, and using points further down the curve. We keep trying this until the found end point is for t=1, at which point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve:

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

So... what is this good for? Obviously, if you're working with technologies that can't do curves, but can do lines and circles, then the answer is pretty straightforward, but what else? There are some reasons why you might need this technique: using circular arcs means you can determine whether a coordinate lies "on" your curve really easily (simply compute the distance to each circular arc center, and if any of those are close to the arc radii, at an angle between the arc start and end, bingo, this point can be treated as lying "on the curve"). Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which, based on your application!

@@ -2323,8 +2332,13 @@ for p = 1 to points.length-3 (inclusive):

B-Splines

No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference, and we'll be looking at those differences in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves (that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them, and how to draw them based on a number of parameters that you can pick for individual B-Splines.

First off: B-Splines are piecewise polynomial interpolation curves, where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.

-

What do they look like? They look like this! .. okay that's an empty graph, but simply click to place some point, with the stipulation that you need at least four point to see any curve. More than four points simply draws a longer B-Spline curve:

- +

What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the spline curve drawn.

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

The important part to notice here is that we are not doing the same thing with B-Splines that we do for poly-Béziers or Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that follows, so we can only have 4, 7, 10, 13, 16, etc point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth interpolations of each possible curve involving four consecutive points, such that at any point along the curve except for our start and end points, our on-curve coordinate is defined by four control points.

Consider the difference to be this:

@@ -2335,31 +2349,33 @@ for p = 1 to points.length-3 (inclusive):

In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work.

How to compute a B-Spline curve: some maths

Given a B-Spline of degree d and thus order k=d+1 (so a quadratic B-Spline is degree 2 and order 3, a cubic B-Spline is degree 3 and order 4, etc) and n control points P0 through Pn-1, we can compute a point on the curve for some value t in the interval [0,1] (where 0 is the start of the curve, and 1 the end, just like for Bézier curves), by evaluating the following function:

- +

Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control points, weighted somehow", where the weighting is achieved through the N(...) function, subscripted with an obvious parameter i, which comes from our summation, and some magical parameter k. So we need to know two things: 1. what does N(t) do, and 2. what is that k? Let's cover both, in reverse order.

The parameter k represents the "knot interval" over which a section of curve is defined. As we learned earlier, a B-Spline curve is itself an interpoliation of curves, and we can treat each transition where a control point starts or stops influencing the total curvature as a "knot on the curve". Doing so for a degree d B-Spline with n control point gives us d + n + 1 knots, defining d + n intervals along the curve, and it is these intervals that the above k subscript to the N() function applies to.

Then the N() function itself. What does it look like?

- +

So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation, on a specific knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that this is a recursive iteration where i goes up, and k goes down, so it seem reasonable to expect that this recursion has to stop at some point; obviously, it does, and specifically it does so for the following i/k values:

- +

And this function finally has a straight up evaluation: if a t value lies within a knot-specific interval once we reach a k=1 value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale our t value first, so that it lies in the interval bounded by knots[d] and knots[n], which are the start point and end point where curvature is controlled by exactly order control points. For instance, for degree 3 (=order 4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map t from [the interval 0,1] to the interval [4,8], and then use that value in the functions above, instead.

Can we simplify that?

We can, yes.

People far smarter than us have looked at this work, and two in particular — Maurice Cox and Carl de Boor — came to a mathematically pleasing solution: to compute a point P(t), we can compute this point by evaluating d(t) on a curve section between knots i and i+1:

- +

This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined by:

- +

That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha". Computing this d() function is thus simply a matter of "computing simple arithmetics but with recursion", which might be computationally expensive because we're doing "a lot of" steps, but is also computationally cheap because each step only involves very simple maths. Of course as before the recursion has to stop:

- +

So, we see two stopping conditions: either i becomes 0, in which case d() is zero, or k becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.

Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:

- +

That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!

One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the d(..., k) values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point vector and then works its way up to that single point: that's pretty essential!

If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate the terminating d() values first, then compute the a() constants, perform our multiplications, generate the previous step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers we start with and simply update them as we move up the triangle. So, let's implement that!

Cool, cool... but I don't know what to do with that information

I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by one function each) influences the total curvature, given our knot values. And, because exploration is the key to discovery, let's make the knot vector a thing we can actually manipulate. Normally a proper knot vector has a constraint that any value is strictly equal to, or larger than the previous ones, but screw it this is programming, let's ignore that hard restriction and just mess with the knots however we like.

+ +
this.bindKnots(owner, knots, "interpolation-graph")}/> @@ -2398,44 +2414,56 @@ for(let L = 1; L <= order; L++) {

Uniform B-Splines

The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines the same intervals, or even [0,2,3,...,18,20,22], which also defines the same intervals, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to the curvature.

-
- - this.bindKnots(owner, knots, "uniform-spline")}/> -
+ + + Scripts are disabled. Showing fallback image. + + + + +

This is an important point: the intervals that the knot vector defines are relative intervals, so it doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve.

The problem with uniform knot vectors is that, as we need order control points before we have any curve with which we can perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get rid of these, by being clever about how we apply the following uniformity-breaking approach instead...

Reducing local curve complexity by collapsing intervals

-

By collapsing knot intervals by making two or more consecutive knots have the same value, we can reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost and the curve "kinks".

-
- - this.bindKnots(owner, knots, "center-cut-bspline")}/> -
+

Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost and the curve "kinks".

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

Open-Uniform B-Splines

By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we can overcome the problem of the curve not starting and ending where we'd kind of like it to:

For any curve of degree D with control points N, we can define a knot vector of length N+D+1 in which the values 0 ... D+1 are the same, the values D+1 ... N+1 follow the "uniform" pattern, and the values N+1 ... N+D+1 are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.

-
- - this.bindKnots(owner, knots, "open-uniform-bspline")}/> -
+ + + Scripts are disabled. Showing fallback image. + + + + + +

Non-uniform B-Splines

-

This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, and that is that any value knots[k+1] should be equal to, or greater than knots[k].

+

This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, other than that any value knots[k+1] should be greater than or equal to knots[k].

One last thing: Rational B-Splines

While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a control point carries, the close to that point the spline curve will lie, a bit like turning up the gravity of a control point.

-
- { - // - } - - { - // this.bindKnots(owner, knots, "rational-uniform-bspline"); - this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights"); - }} /> -
+ + + Scripts are disabled. Showing fallback image. + + + + + -

Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS are an important type of curve in computer-facilitated design, used a lot in 3D modelling (as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

+

Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the last.

Extending our implementation to cover rational splines

The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and the extension to work in the control point weights is fairly simple: we extend each control point from a point in its original number of dimensions (2D, 3D, etc) to one dimension higher, scaling the original dimensions by the control point's weight, and then assigning that weight as its value for the extended dimension.

diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index f4d6ce83..eaad6187 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -1584,7 +1584,7 @@ lli = function(line1, line2): Scripts are disabled. Showing fallback image. - + @@ -2261,10 +2261,10 @@ for p = 1 to points.length-3 (inclusive):

Which, in decimal values, rounded to six significant digits, is:

Of course, this is for a circle with radius 1, so if you have a different radius circle, simply multiply the coordinate by the radius you need. And then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin:

- + Scripts are disabled. Showing fallback image. - + @@ -2274,43 +2274,52 @@ for p = 1 to points.length-3 (inclusive):

Let's look at doing the exact opposite of the previous section: rather than approximating circular arc using Bézier curves, let's approximate Bézier curves using circular arcs.

We already saw in the section on circle approximation that this will never yield a perfect equivalent, but sometimes you need circular arcs, such as when you're working with fabrication machinery, or simple vector languages that understand lines and circles, but not much else.

The approach is fairly simple: pick a starting point on the curve, and pick two points that are further along the curve. Determine the circle that goes through those three points, and see if it fits the part of the curve we're trying to approximate. Decent fit? Try spacing the points further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've found the "good approximation/bad approximation" boundary, record the "good" arc, and then move the starting point up to overlap the end point we previously found. Rinse and repeat until we've covered the entire curve.

-

So: step 1, how do we find a circle through three points? That part is actually really simple. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and 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.

-

So: if we have have three points, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle. So we find the centers of the chords, find the perpendicular lines, find the intersection of those lines, and thus find the center of the circle.

-

The following graphic shows this procedure with a different colour for each chord and its associated perpendicular through the center. You can move the points around as much as you like, those lines will always meet!

- - -

So, with the procedure on how to find a circle through three points, finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, over the remaining point, to the end point.

-

So how can we convert a Bézier curve into a (sequence of) circular arc(s)?

+

We already saw how to fit a circle through three points in the section on creating a curve from three points, and finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, to the end point, over the remaining point.

+

So, how can we convert a Bézier curve into a (sequence of) circular arc(s)?

    -
  • Start at t=0
  • -
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • +
  • Start at t=0
  • +
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • Find the arc that these points define
  • Determine how close the found arc is to the curve:
      -
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • +
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • These points, if the arc is a good approximation of the curve interval chosen, should - lie on the circle, so their distance to the center of the circle should be the + lie on the circle, so their distance to the center of the circle should be the same as the distance from any of the three other points to the center.
    • For point points, determine the (absolute) error between the radius of the circle, and the -actual distance from the center of the circle to the point on the curve.
    • +actual distance from the center of the circle to the point on the curve.
    • If this error is too high, we consider the arc bad, and try a smaller interval.
-

The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a Binary Search.

+

The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a binary search.

    -
  1. We start with {0, 0.5, 1}
  2. -
  3. That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
      -
    • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
    • -
    • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • +
    • We start with low=0, mid=0.5 and high=1
    • +
    • That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
        +
      • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
      • +
      • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • -
    • We keep doing this over and over until we have two arcs found in sequence of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the former.
    • +
    • We keep doing this over and over until we have two arcs, in sequence, of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the good arc.

The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a combined half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is.

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

With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined arc's starting point, and using points further down the curve. We keep trying this until the found end point is for t=1, at which point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve:

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

So... what is this good for? Obviously, if you're working with technologies that can't do curves, but can do lines and circles, then the answer is pretty straightforward, but what else? There are some reasons why you might need this technique: using circular arcs means you can determine whether a coordinate lies "on" your curve really easily (simply compute the distance to each circular arc center, and if any of those are close to the arc radii, at an angle between the arc start and end, bingo, this point can be treated as lying "on the curve"). Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which, based on your application!

@@ -2319,8 +2328,13 @@ for p = 1 to points.length-3 (inclusive):

B-Splines

No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference, and we'll be looking at those differences in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves (that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them, and how to draw them based on a number of parameters that you can pick for individual B-Splines.

First off: B-Splines are piecewise polynomial interpolation curves, where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.

-

What do they look like? They look like this! .. okay that's an empty graph, but simply click to place some point, with the stipulation that you need at least four point to see any curve. More than four points simply draws a longer B-Spline curve:

- +

What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the spline curve drawn.

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

The important part to notice here is that we are not doing the same thing with B-Splines that we do for poly-Béziers or Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that follows, so we can only have 4, 7, 10, 13, 16, etc point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth interpolations of each possible curve involving four consecutive points, such that at any point along the curve except for our start and end points, our on-curve coordinate is defined by four control points.

Consider the difference to be this:

@@ -2331,31 +2345,33 @@ for p = 1 to points.length-3 (inclusive):

In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work.

How to compute a B-Spline curve: some maths

Given a B-Spline of degree d and thus order k=d+1 (so a quadratic B-Spline is degree 2 and order 3, a cubic B-Spline is degree 3 and order 4, etc) and n control points P0 through Pn-1, we can compute a point on the curve for some value t in the interval [0,1] (where 0 is the start of the curve, and 1 the end, just like for Bézier curves), by evaluating the following function:

- +

Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control points, weighted somehow", where the weighting is achieved through the N(...) function, subscripted with an obvious parameter i, which comes from our summation, and some magical parameter k. So we need to know two things: 1. what does N(t) do, and 2. what is that k? Let's cover both, in reverse order.

The parameter k represents the "knot interval" over which a section of curve is defined. As we learned earlier, a B-Spline curve is itself an interpoliation of curves, and we can treat each transition where a control point starts or stops influencing the total curvature as a "knot on the curve". Doing so for a degree d B-Spline with n control point gives us d + n + 1 knots, defining d + n intervals along the curve, and it is these intervals that the above k subscript to the N() function applies to.

Then the N() function itself. What does it look like?

- +

So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation, on a specific knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that this is a recursive iteration where i goes up, and k goes down, so it seem reasonable to expect that this recursion has to stop at some point; obviously, it does, and specifically it does so for the following i/k values:

- +

And this function finally has a straight up evaluation: if a t value lies within a knot-specific interval once we reach a k=1 value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale our t value first, so that it lies in the interval bounded by knots[d] and knots[n], which are the start point and end point where curvature is controlled by exactly order control points. For instance, for degree 3 (=order 4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map t from [the interval 0,1] to the interval [4,8], and then use that value in the functions above, instead.

Can we simplify that?

We can, yes.

People far smarter than us have looked at this work, and two in particular — Maurice Cox and Carl de Boor — came to a mathematically pleasing solution: to compute a point P(t), we can compute this point by evaluating d(t) on a curve section between knots i and i+1:

- +

This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined by:

- +

That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha". Computing this d() function is thus simply a matter of "computing simple arithmetics but with recursion", which might be computationally expensive because we're doing "a lot of" steps, but is also computationally cheap because each step only involves very simple maths. Of course as before the recursion has to stop:

- +

So, we see two stopping conditions: either i becomes 0, in which case d() is zero, or k becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.

Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:

- +

That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!

One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the d(..., k) values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point vector and then works its way up to that single point: that's pretty essential!

If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate the terminating d() values first, then compute the a() constants, perform our multiplications, generate the previous step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers we start with and simply update them as we move up the triangle. So, let's implement that!

Cool, cool... but I don't know what to do with that information

I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by one function each) influences the total curvature, given our knot values. And, because exploration is the key to discovery, let's make the knot vector a thing we can actually manipulate. Normally a proper knot vector has a constraint that any value is strictly equal to, or larger than the previous ones, but screw it this is programming, let's ignore that hard restriction and just mess with the knots however we like.

+ +
this.bindKnots(owner, knots, "interpolation-graph")}/> @@ -2394,44 +2410,56 @@ for(let L = 1; L <= order; L++) {

Uniform B-Splines

The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines the same intervals, or even [0,2,3,...,18,20,22], which also defines the same intervals, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to the curvature.

-
- - this.bindKnots(owner, knots, "uniform-spline")}/> -
+ + + Scripts are disabled. Showing fallback image. + + + + +

This is an important point: the intervals that the knot vector defines are relative intervals, so it doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve.

The problem with uniform knot vectors is that, as we need order control points before we have any curve with which we can perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get rid of these, by being clever about how we apply the following uniformity-breaking approach instead...

Reducing local curve complexity by collapsing intervals

-

By collapsing knot intervals by making two or more consecutive knots have the same value, we can reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost and the curve "kinks".

-
- - this.bindKnots(owner, knots, "center-cut-bspline")}/> -
+

Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost and the curve "kinks".

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

Open-Uniform B-Splines

By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we can overcome the problem of the curve not starting and ending where we'd kind of like it to:

For any curve of degree D with control points N, we can define a knot vector of length N+D+1 in which the values 0 ... D+1 are the same, the values D+1 ... N+1 follow the "uniform" pattern, and the values N+1 ... N+D+1 are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.

-
- - this.bindKnots(owner, knots, "open-uniform-bspline")}/> -
+ + + Scripts are disabled. Showing fallback image. + + + + + +

Non-uniform B-Splines

-

This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, and that is that any value knots[k+1] should be equal to, or greater than knots[k].

+

This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, other than that any value knots[k+1] should be greater than or equal to knots[k].

One last thing: Rational B-Splines

While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a control point carries, the close to that point the spline curve will lie, a bit like turning up the gravity of a control point.

-
- { - // - } - - { - // this.bindKnots(owner, knots, "rational-uniform-bspline"); - this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights"); - }} /> -
+ + + Scripts are disabled. Showing fallback image. + + + + + -

Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS are an important type of curve in computer-facilitated design, used a lot in 3D modelling (as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

+

Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the last.

Extending our implementation to cover rational splines

The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and the extension to work in the control point weights is fairly simple: we extend each control point from a point in its original number of dimensions (2D, 3D, etc) to one dimension higher, scaling the original dimensions by the control point's weight, and then assigning that weight as its value for the extended dimension.

diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 0f69ca4d..8ef60858 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -1,5 +1,6 @@ import { enrich } from "../lib/enrich.js"; import { Bezier } from "./types/bezier.js"; +import { BSpline } from "./types/bspline.js"; import { Vector } from "./types/vector.js"; import { Matrix } from "./types/matrix.js"; import { Shape } from "./util/shape.js"; @@ -791,4 +792,4 @@ class GraphicsAPI extends BaseAPI { } } -export { GraphicsAPI, Bezier, Vector, Matrix, Shape }; +export { GraphicsAPI, Bezier, BSpline, Vector, Matrix, Shape }; diff --git a/docs/js/custom-element/api/types/bspline.js b/docs/js/custom-element/api/types/bspline.js new file mode 100644 index 00000000..91cd1f33 --- /dev/null +++ b/docs/js/custom-element/api/types/bspline.js @@ -0,0 +1,85 @@ +import interpolate from "../util/spline.js"; + +// cubic B-Spline +const DEGREE = 3; + +class BSpline { + constructor(apiInstance, points) { + this.api = apiInstance; + this.ctx = apiInstance.ctx; + + // the spline library needs points in array format [x,y] rather than object format {x:..., y:...} + this.points = points.map((v) => { + if (v instanceof Array) return v; + return [v.x, v.y]; + }); + } + + getLUT(count) { + let c = count - 1; + return [...new Array(count)].map((_, i) => { + let p = interpolate(i / c, DEGREE, this.points, this.knots, this.weights); + return { x: p[0], y: p[1] }; + }); + } + + formKnots(open = false) { + if (!open) return this.formUniformKnots(); + + let knots = [], + l = this.points.length, + m = l - DEGREE; + + // form the open-uniform knot vector + for (let i = 1; i < l - DEGREE; i++) { + knots.push(i + DEGREE); + } + // add [degree] zeroes at the front + for (let i = 0; i <= DEGREE; i++) { + knots = [DEGREE].concat(knots); + } + // add [degree] max-values to the back + for (let i = 0; i <= DEGREE; i++) { + knots.push(m + DEGREE); + } + + return (this.knots = knots); + } + + formUniformKnots() { + return (this.knots = [...new Array(this.points.length + DEGREE + 1)].map( + (_, i) => i + )); + } + + formNodes() { + const knots = this.knots; + const domain = [DEGREE, knots.length - 1 - DEGREE], + nodes = []; + + for (let k = 0; k < this.points.length; k++) { + let node = 0; + for (let offset = 1; offset <= DEGREE; offset++) { + node += knots[k + offset]; + } + node /= DEGREE; + if (node < knots[domain[0]]) continue; + if (node > knots[domain[1]]) continue; + nodes.push(node); + } + + return (this.nodes = nodes); + } + + formWeights() { + return (this.weights = this.points.map((p) => 1)); + } + + setDegree(d) { + DEGREE += d; + this.knots = this.formKnots(); + this.nodes = this.formNodes(); + } +} + +export { BSpline }; diff --git a/docs/js/custom-element/api/util/spline.js b/docs/js/custom-element/api/util/spline.js new file mode 100644 index 00000000..3b16f754 --- /dev/null +++ b/docs/js/custom-element/api/util/spline.js @@ -0,0 +1,88 @@ +// https://github.com/thibauts/b-spline +export default function interpolate( + t, + degree, + points, + knots, + weights, + result, + scaled +) { + var i, j, s, l; // function-scoped iteration variables + var n = points.length; // points count + var d = points[0].length; // point dimensionality + + if (degree < 1) throw new Error("degree must be at least 1 (linear)"); + if (degree > n - 1) + throw new Error("degree must be less than or equal to point count - 1"); + + if (!weights) { + // build weight vector of length [n] + weights = []; + for (i = 0; i < n; i++) { + weights[i] = 1; + } + } + + if (!knots) { + // build knot vector of length [n + degree + 1] + var knots = []; + for (i = 0; i < n + degree + 1; i++) { + knots[i] = i; + } + } else { + if (knots.length !== n + degree + 1) + throw new Error("bad knot vector length"); + } + + var domain = [degree, knots.length - 1 - degree]; + + var low = knots[domain[0]]; + var high = knots[domain[1]]; + + // remap t to the domain where the spline is defined + if (!scaled) { + t = t * (high - low) + low; + } + + if (t < low || t > high) throw new Error("out of bounds"); + + // find s (the spline segment) for the [t] value provided + for (s = domain[0]; s < domain[1]; s++) { + if (t >= knots[s] && t <= knots[s + 1]) { + break; + } + } + + // convert points to homogeneous coordinates + var v = []; + for (i = 0; i < n; i++) { + v[i] = []; + for (j = 0; j < d; j++) { + v[i][j] = points[i][j] * weights[i]; + } + v[i][d] = weights[i]; + } + + // l (level) goes from 1 to the curve degree + 1 + var alpha; + for (l = 1; l <= degree + 1; l++) { + // build level l of the pyramid + for (i = s; i > s - degree - 1 + l; i--) { + alpha = (t - knots[i]) / (knots[i + degree + 1 - l] - knots[i]); + + // interpolate each component + for (j = 0; j < d + 1; j++) { + v[i][j] = (1 - alpha) * v[i - 1][j] + alpha * v[i][j]; + } + } + } + + // convert back to cartesian and return + var result = result || []; + for (i = 0; i < d; i++) { + result[i] = v[s][i] / v[s][d]; + } + + return result; +} diff --git a/docs/js/custom-element/graphics-element.js b/docs/js/custom-element/graphics-element.js index c6f81bba..e952b697 100644 --- a/docs/js/custom-element/graphics-element.js +++ b/docs/js/custom-element/graphics-element.js @@ -207,7 +207,7 @@ class GraphicsElement extends CustomElement { * Program source: ${src} * Data attributes: ${JSON.stringify(this.dataset)} */ - import { GraphicsAPI, Bezier, Vector, Matrix, Shape } from "${MODULE_PATH}/api/graphics-api.js"; + import { GraphicsAPI, Bezier, BSpline, Vector, Matrix, Shape } from "${MODULE_PATH}/api/graphics-api.js"; ${globalCode} diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index f7a8c269..8c628ff1 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -1578,7 +1578,7 @@ lli = function(line1, line2): Scripts are disabled. Showing fallback image. - + @@ -2255,10 +2255,10 @@ for p = 1 to points.length-3 (inclusive):

Which, in decimal values, rounded to six significant digits, is:

Of course, this is for a circle with radius 1, so if you have a different radius circle, simply multiply the coordinate by the radius you need. And then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin:

- + Scripts are disabled. Showing fallback image. - + @@ -2268,43 +2268,52 @@ for p = 1 to points.length-3 (inclusive):

Let's look at doing the exact opposite of the previous section: rather than approximating circular arc using Bézier curves, let's approximate Bézier curves using circular arcs.

We already saw in the section on circle approximation that this will never yield a perfect equivalent, but sometimes you need circular arcs, such as when you're working with fabrication machinery, or simple vector languages that understand lines and circles, but not much else.

The approach is fairly simple: pick a starting point on the curve, and pick two points that are further along the curve. Determine the circle that goes through those three points, and see if it fits the part of the curve we're trying to approximate. Decent fit? Try spacing the points further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've found the "good approximation/bad approximation" boundary, record the "good" arc, and then move the starting point up to overlap the end point we previously found. Rinse and repeat until we've covered the entire curve.

-

So: step 1, how do we find a circle through three points? That part is actually really simple. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and 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.

-

So: if we have have three points, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle. So we find the centers of the chords, find the perpendicular lines, find the intersection of those lines, and thus find the center of the circle.

-

The following graphic shows this procedure with a different colour for each chord and its associated perpendicular through the center. You can move the points around as much as you like, those lines will always meet!

- - -

So, with the procedure on how to find a circle through three points, finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, over the remaining point, to the end point.

-

So how can we convert a Bézier curve into a (sequence of) circular arc(s)?

+

We already saw how to fit a circle through three points in the section on creating a curve from three points, and finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, to the end point, over the remaining point.

+

So, how can we convert a Bézier curve into a (sequence of) circular arc(s)?

    -
  • Start at t=0
  • -
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • +
  • Start at t=0
  • +
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • Find the arc that these points define
  • Determine how close the found arc is to the curve:
      -
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • +
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • These points, if the arc is a good approximation of the curve interval chosen, should - lie on the circle, so their distance to the center of the circle should be the + lie on the circle, so their distance to the center of the circle should be the same as the distance from any of the three other points to the center.
    • For point points, determine the (absolute) error between the radius of the circle, and the -actual distance from the center of the circle to the point on the curve.
    • +actual distance from the center of the circle to the point on the curve.
    • If this error is too high, we consider the arc bad, and try a smaller interval.
-

The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a Binary Search.

+

The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is simply at t=0.5, and then we start performing a binary search.

    -
  1. We start with {0, 0.5, 1}
  2. -
  3. That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
      -
    • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
    • -
    • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • +
    • We start with low=0, mid=0.5 and high=1
    • +
    • That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
        +
      • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
      • +
      • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • -
    • We keep doing this over and over until we have two arcs found in sequence of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the former.
    • +
    • We keep doing this over and over until we have two arcs, in sequence, of which the first arc is good, and the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the good arc.

The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a combined half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is.

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

With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined arc's starting point, and using points further down the curve. We keep trying this until the found end point is for t=1, at which point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve:

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

So... what is this good for? Obviously, if you're working with technologies that can't do curves, but can do lines and circles, then the answer is pretty straightforward, but what else? There are some reasons why you might need this technique: using circular arcs means you can determine whether a coordinate lies "on" your curve really easily (simply compute the distance to each circular arc center, and if any of those are close to the arc radii, at an angle between the arc start and end, bingo, this point can be treated as lying "on the curve"). Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which, based on your application!

@@ -2313,8 +2322,13 @@ for p = 1 to points.length-3 (inclusive):

B-Splines

No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference, and we'll be looking at those differences in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves (that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them, and how to draw them based on a number of parameters that you can pick for individual B-Splines.

First off: B-Splines are piecewise polynomial interpolation curves, where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.

-

What do they look like? They look like this! .. okay that's an empty graph, but simply click to place some point, with the stipulation that you need at least four point to see any curve. More than four points simply draws a longer B-Spline curve:

- +

What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the spline curve drawn.

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

The important part to notice here is that we are not doing the same thing with B-Splines that we do for poly-Béziers or Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that follows, so we can only have 4, 7, 10, 13, 16, etc point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth interpolations of each possible curve involving four consecutive points, such that at any point along the curve except for our start and end points, our on-curve coordinate is defined by four control points.

Consider the difference to be this:

@@ -2325,31 +2339,33 @@ for p = 1 to points.length-3 (inclusive):

In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work.

How to compute a B-Spline curve: some maths

Given a B-Spline of degree d and thus order k=d+1 (so a quadratic B-Spline is degree 2 and order 3, a cubic B-Spline is degree 3 and order 4, etc) and n control points P0 through Pn-1, we can compute a point on the curve for some value t in the interval [0,1] (where 0 is the start of the curve, and 1 the end, just like for Bézier curves), by evaluating the following function:

- +

Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control points, weighted somehow", where the weighting is achieved through the N(...) function, subscripted with an obvious parameter i, which comes from our summation, and some magical parameter k. So we need to know two things: 1. what does N(t) do, and 2. what is that k? Let's cover both, in reverse order.

The parameter k represents the "knot interval" over which a section of curve is defined. As we learned earlier, a B-Spline curve is itself an interpoliation of curves, and we can treat each transition where a control point starts or stops influencing the total curvature as a "knot on the curve". Doing so for a degree d B-Spline with n control point gives us d + n + 1 knots, defining d + n intervals along the curve, and it is these intervals that the above k subscript to the N() function applies to.

Then the N() function itself. What does it look like?

- +

So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation, on a specific knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that this is a recursive iteration where i goes up, and k goes down, so it seem reasonable to expect that this recursion has to stop at some point; obviously, it does, and specifically it does so for the following i/k values:

- +

And this function finally has a straight up evaluation: if a t value lies within a knot-specific interval once we reach a k=1 value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale our t value first, so that it lies in the interval bounded by knots[d] and knots[n], which are the start point and end point where curvature is controlled by exactly order control points. For instance, for degree 3 (=order 4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map t from [the interval 0,1] to the interval [4,8], and then use that value in the functions above, instead.

Can we simplify that?

We can, yes.

People far smarter than us have looked at this work, and two in particular — Maurice Cox and Carl de Boor — came to a mathematically pleasing solution: to compute a point P(t), we can compute this point by evaluating d(t) on a curve section between knots i and i+1:

- +

This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined by:

- +

That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha". Computing this d() function is thus simply a matter of "computing simple arithmetics but with recursion", which might be computationally expensive because we're doing "a lot of" steps, but is also computationally cheap because each step only involves very simple maths. Of course as before the recursion has to stop:

- +

So, we see two stopping conditions: either i becomes 0, in which case d() is zero, or k becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.

Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:

- +

That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!

One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the d(..., k) values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point vector and then works its way up to that single point: that's pretty essential!

If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate the terminating d() values first, then compute the a() constants, perform our multiplications, generate the previous step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers we start with and simply update them as we move up the triangle. So, let's implement that!

Cool, cool... but I don't know what to do with that information

I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by one function each) influences the total curvature, given our knot values. And, because exploration is the key to discovery, let's make the knot vector a thing we can actually manipulate. Normally a proper knot vector has a constraint that any value is strictly equal to, or larger than the previous ones, but screw it this is programming, let's ignore that hard restriction and just mess with the knots however we like.

+ +
this.bindKnots(owner, knots, "interpolation-graph")}/> @@ -2388,44 +2404,56 @@ for(let L = 1; L <= order; L++) {

Uniform B-Splines

The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines the same intervals, or even [0,2,3,...,18,20,22], which also defines the same intervals, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to the curvature.

-
- - this.bindKnots(owner, knots, "uniform-spline")}/> -
+ + + Scripts are disabled. Showing fallback image. + + + + +

This is an important point: the intervals that the knot vector defines are relative intervals, so it doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve.

The problem with uniform knot vectors is that, as we need order control points before we have any curve with which we can perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get rid of these, by being clever about how we apply the following uniformity-breaking approach instead...

Reducing local curve complexity by collapsing intervals

-

By collapsing knot intervals by making two or more consecutive knots have the same value, we can reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost and the curve "kinks".

-
- - this.bindKnots(owner, knots, "center-cut-bspline")}/> -
+

Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost and the curve "kinks".

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

Open-Uniform B-Splines

By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we can overcome the problem of the curve not starting and ending where we'd kind of like it to:

For any curve of degree D with control points N, we can define a knot vector of length N+D+1 in which the values 0 ... D+1 are the same, the values D+1 ... N+1 follow the "uniform" pattern, and the values N+1 ... N+D+1 are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.

-
- - this.bindKnots(owner, knots, "open-uniform-bspline")}/> -
+ + + Scripts are disabled. Showing fallback image. + + + + + +

Non-uniform B-Splines

-

This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, and that is that any value knots[k+1] should be equal to, or greater than knots[k].

+

This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, other than that any value knots[k+1] should be greater than or equal to knots[k].

One last thing: Rational B-Splines

While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a control point carries, the close to that point the spline curve will lie, a bit like turning up the gravity of a control point.

-
- { - // - } - - { - // this.bindKnots(owner, knots, "rational-uniform-bspline"); - this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights"); - }} /> -
+ + + Scripts are disabled. Showing fallback image. + + + + + -

Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS are an important type of curve in computer-facilitated design, used a lot in 3D modelling (as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

+

Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the last.

Extending our implementation to cover rational splines

The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and the extension to work in the control point weights is fairly simple: we extend each control point from a point in its original number of dimensions (2D, 3D, etc) to one dimension higher, scaling the original dimensions by the control point's weight, and then assigning that weight as its value for the extended dimension.

diff --git a/src/build/markdown/generate-graphics-module.js b/src/build/markdown/generate-graphics-module.js index 4f9c6e15..eab63349 100644 --- a/src/build/markdown/generate-graphics-module.js +++ b/src/build/markdown/generate-graphics-module.js @@ -36,7 +36,7 @@ function generateGraphicsModule(chapter, code, width, height, dataset) { let moduleCode = ` import CanvasBuilder from 'canvas'; - import { GraphicsAPI, Bezier, Vector, Matrix, Shape } from "${GRAPHICS_API_LOCATION}"; + import { GraphicsAPI, Bezier, BSpline, Vector, Matrix, Shape } from "${GRAPHICS_API_LOCATION}"; ${globalCode}