From 1de1fc9ce3ed711281ab03fd8f21b74359502a04 Mon Sep 17 00:00:00 2001 From: Pomax Date: Sun, 6 Sep 2020 09:08:11 -0700 Subject: [PATCH] splines + slider refinement --- docs/chapters/bsplines/content.en-GB.md | 17 ++- docs/chapters/bsplines/interpolation-graph.js | 113 ---------------- docs/chapters/bsplines/interpolation.js | 122 ++++++++++++++++++ .../bsplines/rational-non-uniform-bspline.js | 42 ------ docs/chapters/bsplines/rational-uniform.js | 15 ++- docs/chapters/bsplines/reduced.js | 33 +++-- docs/chapters/bsplines/uniform.js | 17 ++- docs/chapters/curvefitting/curve-fitting.js | 53 ++++---- ...g => 4c71f82901edc1a0acae31cf5be12c65.png} | Bin ...g => 7ac6e46280de14dce141fb073777dc8e.png} | Bin .../84bd623b9d3d8bf01a656f49c17079b8.png | Bin 0 -> 31900 bytes ...g => 984c86b3b357c799aaab5a5fbd48d599.png} | Bin ...g => 9915d887443459415a7bfb57bea1b898.png} | Bin ...g => 78d32beb061391c47217611128446146.png} | Bin docs/index.html | 32 +++-- docs/ja-JP/index.html | 32 +++-- docs/js/custom-element/api/graphics-api.js | 48 ++++++- docs/js/custom-element/api/types/bspline.js | 2 +- .../{spline.js => interpolate-bspline.js} | 10 ++ docs/js/custom-element/lib/create.js | 14 ++ docs/zh-CN/index.html | 32 +++-- 21 files changed, 324 insertions(+), 258 deletions(-) delete mode 100644 docs/chapters/bsplines/interpolation-graph.js create mode 100644 docs/chapters/bsplines/interpolation.js delete mode 100644 docs/chapters/bsplines/rational-non-uniform-bspline.js rename docs/images/chapters/bsplines/{8caa3e8ff614ad9731b15dacaba98c3c.png => 4c71f82901edc1a0acae31cf5be12c65.png} (100%) rename docs/images/chapters/bsplines/{5d3b04c3161a3429ce651bb7a5fa0399.png => 7ac6e46280de14dce141fb073777dc8e.png} (100%) create mode 100644 docs/images/chapters/bsplines/84bd623b9d3d8bf01a656f49c17079b8.png rename docs/images/chapters/bsplines/{93146ea89bb21999d9e18b57dd1bdd29.png => 984c86b3b357c799aaab5a5fbd48d599.png} (100%) rename docs/images/chapters/bsplines/{fc654445500dd595d6ae9de27a3dc46c.png => 9915d887443459415a7bfb57bea1b898.png} (100%) rename docs/images/chapters/curvefitting/{c6c8442e24793ce72a872ce29b2b4125.png => 78d32beb061391c47217611128446146.png} (100%) rename docs/js/custom-element/api/util/{spline.js => interpolate-bspline.js} (90%) create mode 100644 docs/js/custom-element/lib/create.js diff --git a/docs/chapters/bsplines/content.en-GB.md b/docs/chapters/bsplines/content.en-GB.md index 5502fc08..ccf52a68 100644 --- a/docs/chapters/bsplines/content.en-GB.md +++ b/docs/chapters/bsplines/content.en-GB.md @@ -103,14 +103,13 @@ If we run this computation "down", starting at d(3,3), then without special code ## 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. +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 (you will notice that knots are constrained in their value: any knot must strictly be equal to, or greater than, the previous value). -
- - this.bindKnots(owner, knots, "interpolation-graph")}/> -
+ + + Changing the values in the knot vector changes how much each point influences the total curvature (with some clever knot value manipulation, we can even make the influence of certain points disappear entirely!), so we can see that while the control points define the hull inside of which we're going to be drawing a curve, it is actually the knot vector that determines the actual *shape* of the curve inside that hull. @@ -149,9 +148,9 @@ for(let L = 1; L <= order; L++) { ## 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! +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 coordinate. However, because B-Splines are an interpolation of curves, not just points, we can't simply make the first and last point the same, we need to link as many points as are necessary to form "a curve" that the spline performs interpolation with. As such, for an order `d` B-Spline, we need to make the first and last `d` points the same. This is of course hardly more work than before (simply append `points.splice(0,d)` to `points`) but it's important to remember that you need more than just a single point. -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. +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. don't just happen to have the same x/y values, but really are the same coordinate, so that manipulating one will equally manipulate the other, but programming generally makes this really easy by storing references to points, rather than copies (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 @@ -203,9 +202,9 @@ This is essentially the "free form" version of a B-Spline, and also the least in ## 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. +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 closer to that point the spline curve will lie, a bit like turning up the gravity of a control pointl, just like for rational Bézier curves. - + diff --git a/docs/chapters/bsplines/interpolation-graph.js b/docs/chapters/bsplines/interpolation-graph.js deleted file mode 100644 index f08f90c8..00000000 --- a/docs/chapters/bsplines/interpolation-graph.js +++ /dev/null @@ -1,113 +0,0 @@ -var colors = [ - '#C00', - '#CC0', - '#0C0', - '#0CC', - '#00C', - '#C0C', - '#600', - '#660', - '#060', - '#066', - '#006', - '#606' -]; - -module.exports = { - degree: 3, - activeDistance: 9, - cache: { N: [] }, - - setup() { - this.size(600, 300); - this.points = [ - {x:0, y: 0}, - {x:100, y:-100}, - {x:200, y: 100}, - {x:300, y:-100}, - {x:400, y: 100}, - {x:500, y: 0} - ]; - this.knots = this.formKnots(this.points); - if(this.props.controller) { - this.props.controller(this, this.knots); - } - this.draw(); - }, - - draw() { - this.clear(); - var pad = 25; - this.grid(pad); - this.stroke(0); - this.line(pad,0,pad,this.height); - var y = this.height - pad; - this.line(0,y,this.width,y); - - var k = this.degree; - var n = this.points.length || 4; - - for (let i=0; i { + addSlider(`slide-control`, false, min, max, 0.01, i, (v) => this.setKnotValue(i,v)); + }); +} + +setKnotValue(i, v) { + if (i>0 && v < knots[i-1]) throw {value: knots[i-1]}; + if (i knots[i+1]) throw {value: knots[i+1]}; + knots[i] = v; + redraw(); +} + +draw() { + clear(); + setStroke(`lightgrey`); + drawGrid(pad); + + setStroke(`black`); + this.line(pad,0,pad,this.height); + var y = this.height - pad; + this.line(0,y,this.width,y); + + var degree = 3; + var n = points.length || 4; + + for (let i=0, e=n+degree+1; 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.js b/docs/chapters/bsplines/rational-uniform.js index 5ff300ad..195ed98a 100644 --- a/docs/chapters/bsplines/rational-uniform.js +++ b/docs/chapters/bsplines/rational-uniform.js @@ -1,4 +1,4 @@ -let points=[]; +let points=[], weights; setup() { var r = this.width/3; @@ -8,10 +8,21 @@ setup() { y: this.height/2 + r * Math.sin(i/6 * TAU) }); } + + weights = new BSpline(this, points, !!this.parameters.open).formWeights(); + + points.forEach((_,i) => { + addSlider(`slide-control`, false, 0, 10, 0.1, i%2===1? 2 : 6, v => this.setWeight(i, v)); + }); + points = points.concat(points.slice(0,3)); setMovable(points); } +setWeight(i, v) { + weights[i] = v; +} + draw() { clear(); @@ -33,7 +44,7 @@ draw() { drawSplineData() { const spline = new BSpline(this, points, !!this.parameters.open); - spline.formWeights(); + spline.weights = weights; noFill(); setStroke(`black`); diff --git a/docs/chapters/bsplines/reduced.js b/docs/chapters/bsplines/reduced.js index 51566015..d9835f33 100644 --- a/docs/chapters/bsplines/reduced.js +++ b/docs/chapters/bsplines/reduced.js @@ -8,6 +8,27 @@ setup() { }); } setMovable(points); + + knots = new BSpline(this, points).formKnots(!!this.parameters.open); + + const m = round(points.length/2); + knots[m+1] = knots[m]; + knots[m+2] = knots[m]; + for (let i=m+3; i { + addSlider(`slide-control`, false, min, max, 0.01, knots[i], v => this.setKnotValue(i, v)); + }); +} + +setKnotValue(i, v) { + if (i>0 && v < knots[i-1]) throw {value: knots[i-1]}; + if (i knots[i+1]) throw {value: knots[i+1]}; + knots[i] = v; + redraw(); } draw() { @@ -30,16 +51,8 @@ draw() { } 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 { + addSlider(`slide-control`, false, min, max, 0.01, knots[i], v => this.setKnotValue(i, v)); + }); +} + +setKnotValue(i, v) { + if (i>0 && v < knots[i-1]) throw {value: knots[i-1]}; + if (i knots[i+1]) throw {value: knots[i+1]}; + knots[i] = v; + redraw(); } draw() { @@ -31,7 +44,7 @@ draw() { drawSplineData() { const spline = new BSpline(this, points); - spline.formKnots(!!this.parameters.open); + spline.knots = knots; noFill(); setStroke(`black`); diff --git a/docs/chapters/curvefitting/curve-fitting.js b/docs/chapters/curvefitting/curve-fitting.js index 2b73bcbc..6078b57a 100644 --- a/docs/chapters/curvefitting/curve-fitting.js +++ b/docs/chapters/curvefitting/curve-fitting.js @@ -1,16 +1,15 @@ -let points = [], curve, sliders; +let points=[], curve, tvalues=[]; setup() { let btn = find(`.toggle`); if (btn) btn.listen(`click`, evt => this.toggle()); - sliders = find(`.sliders`); this.mode = 0; this.label = `Using equidistant t values`; } toggle() { this.mode = (this.mode + 1) % 2; - if (sliders) this.setSliderValues(this.mode); + this.setSliderValues(this.mode); redraw(); } @@ -23,7 +22,7 @@ draw() { setFontSize(16); setTextStroke(`white`, 4); const n = points.length; - if (n > 2 && sliders && sliders.values) { + if (n > 2) { curve = this.fitCurveToPoints(n); curve.drawSkeleton(`blue`); curve.drawCurve(); @@ -35,7 +34,7 @@ draw() { fitCurveToPoints(n) { // alright, let's do this thing: - const tm = this.formTMatrix(sliders.values, n), + const tm = this.formTMatrix(tvalues, n), T = tm.T, Tt = tm.Tt, M = this.generateBasisMatrix(n), @@ -107,28 +106,20 @@ onMouseDown() { updateSliders() { - if (sliders && points.length > 2) { - sliders.innerHTML = ``; - sliders.values = []; - this.sliders = points.map((p,i) => { - // TODO: this should probably be built into the graphics API as a - // things that you can do, e.g. clearSliders() and addSlider() - let s = document.createElement(`input`); - s.setAttribute(`type`, `range`); - s.setAttribute(`min`, `0`); - s.setAttribute(`max`, `1`); - s.setAttribute(`step`, `0.01`); - s.classList.add(`slide-control`); - sliders.values[i] = i/(points.length-1); - s.setAttribute(`value`, sliders.values[i]); - s.addEventListener(`input`, evt => { - this.label = `Using custom t values`; - sliders.values[i] = parseFloat(evt.target.value); - redraw(); - }); - sliders.append(s); + removeSliders(); + const l = points.length-1; + if (l >= 2) { + points.forEach((_,i) => { + addSlider(`slide-control`, false, 0, 1, 0.01, i/l, v => this.setTvalue(i, v)); }); } + this.label = `Using equidistant t values`; +} + +setTvalue(i, t) { + this.label = `Using custom t values`; + tvalues[i] = t; + redraw(); } setSliderValues(mode) { @@ -137,7 +128,7 @@ setSliderValues(mode) { // equidistant if (mode === 0) { this.label = `Using equidistant t values`; - sliders.values = [...new Array(n)].map((_,i) =>i/(n-1)); + tvalues = [...new Array(n)].map((_,i) =>i/(n-1)); } // polygonal distance @@ -152,11 +143,11 @@ setSliderValues(mode) { } const S = [], len = D[n-1]; D.forEach((v,i) => { S[i] = v/len; }); - sliders.values = S; + tvalues = S; } - findAll(`.sliders input[type=range]`).forEach((s,i) => { - s.setAttribute(`value`, sliders.values[i]); - s.value = sliders.values[i]; + findAll(`input[type=range]`).forEach((s,i) => { + s.setAttribute(`value`, tvalues[i]); + s.value = tvalues[i]; }); -} \ No newline at end of file +} diff --git a/docs/images/chapters/bsplines/8caa3e8ff614ad9731b15dacaba98c3c.png b/docs/images/chapters/bsplines/4c71f82901edc1a0acae31cf5be12c65.png similarity index 100% rename from docs/images/chapters/bsplines/8caa3e8ff614ad9731b15dacaba98c3c.png rename to docs/images/chapters/bsplines/4c71f82901edc1a0acae31cf5be12c65.png diff --git a/docs/images/chapters/bsplines/5d3b04c3161a3429ce651bb7a5fa0399.png b/docs/images/chapters/bsplines/7ac6e46280de14dce141fb073777dc8e.png similarity index 100% rename from docs/images/chapters/bsplines/5d3b04c3161a3429ce651bb7a5fa0399.png rename to docs/images/chapters/bsplines/7ac6e46280de14dce141fb073777dc8e.png diff --git a/docs/images/chapters/bsplines/84bd623b9d3d8bf01a656f49c17079b8.png b/docs/images/chapters/bsplines/84bd623b9d3d8bf01a656f49c17079b8.png new file mode 100644 index 0000000000000000000000000000000000000000..288d16aad8f1562f35b7fa3037125a6ff510b7e0 GIT binary patch literal 31900 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 Scripts are disabled. Showing fallback image. - + @@ -2373,13 +2373,17 @@ Doing so for a degree d B-Spline with n control point

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.

+

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 (you will notice that knots are constrained in their value: any knot must strictly be equal to, or greater than, the previous value).

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

Changing the values in the knot vector changes how much each point influences the total curvature (with some clever knot value manipulation, we can even make the influence of certain points disappear entirely!), so we can see that while the control points define the hull inside of which we're going to be drawing a curve, it is actually the knot vector that determines the actual shape of the curve inside that hull.

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!

@@ -2402,8 +2406,8 @@ 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.

+

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 coordinate. However, because B-Splines are an interpolation of curves, not just points, we can't simply make the first and last point the same, we need to link as many points as are necessary to form "a curve" that the spline performs interpolation with. As such, for an order d B-Spline, we need to make the first and last d points the same. This is of course hardly more work than before (simply append points.splice(0,d) to points) but it's important to remember that you need more than just a single point.

+

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. don't just happen to have the same x/y values, but really are the same coordinate, so that manipulating one will equally manipulate the other, but programming generally makes this really easy by storing references to points, rather than copies (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:

    @@ -2417,7 +2421,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2430,7 +2434,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2443,7 +2447,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2453,11 +2457,11 @@ for(let L = 1; L <= order; L++) {

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

    - +

    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 closer to that point the spline curve will lie, a bit like turning up the gravity of a control pointl, just like for rational Bézier curves.

    + Scripts are disabled. Showing fallback image. - + diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index eaad6187..46235b53 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -1843,7 +1843,7 @@ for (coordinate, index) in LUT: Scripts are disabled. Showing fallback image. - + @@ -2369,13 +2369,17 @@ Doing so for a degree d B-Spline with n control point

    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.

    +

    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 (you will notice that knots are constrained in their value: any knot must strictly be equal to, or greater than, the previous value).

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

    Changing the values in the knot vector changes how much each point influences the total curvature (with some clever knot value manipulation, we can even make the influence of certain points disappear entirely!), so we can see that while the control points define the hull inside of which we're going to be drawing a curve, it is actually the knot vector that determines the actual shape of the curve inside that hull.

    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!

    @@ -2398,8 +2402,8 @@ 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.

    +

    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 coordinate. However, because B-Splines are an interpolation of curves, not just points, we can't simply make the first and last point the same, we need to link as many points as are necessary to form "a curve" that the spline performs interpolation with. As such, for an order d B-Spline, we need to make the first and last d points the same. This is of course hardly more work than before (simply append points.splice(0,d) to points) but it's important to remember that you need more than just a single point.

    +

    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. don't just happen to have the same x/y values, but really are the same coordinate, so that manipulating one will equally manipulate the other, but programming generally makes this really easy by storing references to points, rather than copies (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:

      @@ -2413,7 +2417,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2426,7 +2430,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2439,7 +2443,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2449,11 +2453,11 @@ for(let L = 1; L <= order; L++) {

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

      - +

      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 closer to that point the spline curve will lie, a bit like turning up the gravity of a control pointl, just like for rational Bézier curves.

      + Scripts are disabled. Showing fallback image. - + diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 8ef60858..49060fec 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -1,4 +1,5 @@ import { enrich } from "../lib/enrich.js"; +import { create } from "../lib/create.js"; import { Bezier } from "./types/bezier.js"; import { BSpline } from "./types/bspline.js"; import { Vector } from "./types/vector.js"; @@ -176,6 +177,23 @@ class GraphicsAPI extends BaseAPI { this.line(0, 0, 0, this.height); } + /** + * Dynamically add a slider + */ + addSlider(classes, propname, min, max, step, value, transform) { + if (this.element) { + let slider = create(`input`); + slider.type = `range`; + slider.min = min; + slider.max = max; + slider.step = step; + slider.setAttribute(`value`, value); + slider.setAttribute(`class`, classes); + this.element.append(slider); + this.setSlider(slider, propname, value, transform); + } + } + /** * Set up a slider to control a named, numerical property in the sketch. * @@ -185,22 +203,31 @@ class GraphicsAPI extends BaseAPI { * @param {boolean} redraw whether or not to redraw after updating the value from the slider. */ setSlider(qs, propname, initial, transform) { - if (typeof this[propname] !== `undefined`) { + if (propname !== false && typeof this[propname] !== `undefined`) { throw new Error(`this.${propname} already exists: cannot bind slider.`); } - let slider = this.find(qs); + let slider = typeof qs === `string` ? this.find(qs) : qs; if (!slider) { console.warn(`Warning: no slider found for query selector "${qs}"`); - this[propname] = initial; + if (propname) this[propname] = initial; return undefined; } const updateProperty = (evt) => { let value = parseFloat(slider.value); - slider.setAttribute(`value`, value); - this[propname] = transform ? transform(value) : value; + try { + let checked = transform ? transform(value) ?? value : value; + if (propname) this[propname] = checked; + } catch (e) { + if (evt instanceof Event) { + evt.preventDefault(); + evt.stopPropagation(); + } + slider.value = e.value; + slider.setAttribute(`value`, e.value); + } if (!this.redrawing) this.redraw(); }; @@ -211,6 +238,15 @@ class GraphicsAPI extends BaseAPI { return slider; } + /** + * remove all sliders from this element + */ + removeSliders() { + this.findAll(`input[type=range]`).forEach((s) => { + s.parentNode.removeChild(s); + }); + } + /** * Convert the canvas to an image */ @@ -413,7 +449,7 @@ class GraphicsAPI extends BaseAPI { * Set the context lineWidth */ setWidth(width) { - this.ctx.lineWidth = `${width}px`; + this.ctx.lineWidth = width; } /** diff --git a/docs/js/custom-element/api/types/bspline.js b/docs/js/custom-element/api/types/bspline.js index 91cd1f33..103eabe6 100644 --- a/docs/js/custom-element/api/types/bspline.js +++ b/docs/js/custom-element/api/types/bspline.js @@ -1,4 +1,4 @@ -import interpolate from "../util/spline.js"; +import interpolate from "../util/interpolate-bspline.js"; // cubic B-Spline const DEGREE = 3; diff --git a/docs/js/custom-element/api/util/spline.js b/docs/js/custom-element/api/util/interpolate-bspline.js similarity index 90% rename from docs/js/custom-element/api/util/spline.js rename to docs/js/custom-element/api/util/interpolate-bspline.js index 3b16f754..03009707 100644 --- a/docs/js/custom-element/api/util/spline.js +++ b/docs/js/custom-element/api/util/interpolate-bspline.js @@ -24,6 +24,11 @@ export default function interpolate( } } + // closed curve? + if (weights.length < points.length) { + weights = weights.concat(weights.slice(0, degree)); + } + if (!knots) { // build knot vector of length [n + degree + 1] var knots = []; @@ -35,6 +40,11 @@ export default function interpolate( throw new Error("bad knot vector length"); } + // closed curve? + if (knots.length === points.length) { + knots = knots.concat(knots.slice(0, degree)); + } + var domain = [degree, knots.length - 1 - degree]; var low = knots[domain[0]]; diff --git a/docs/js/custom-element/lib/create.js b/docs/js/custom-element/lib/create.js new file mode 100644 index 00000000..b08bf864 --- /dev/null +++ b/docs/js/custom-element/lib/create.js @@ -0,0 +1,14 @@ +import { enrich } from "./enrich.js"; + +function create(element) { + if (typeof document !== `undefined`) { + return enrich(document.createElement(element)); + } + + return { + name: element, + tag: element.toUpperCase(), + }; +} + +export { create }; diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index 8c628ff1..855f51bf 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -1837,7 +1837,7 @@ for (coordinate, index) in LUT: Scripts are disabled. Showing fallback image. - + @@ -2363,13 +2363,17 @@ Doing so for a degree d B-Spline with n control point

      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.

      +

      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 (you will notice that knots are constrained in their value: any knot must strictly be equal to, or greater than, the previous value).

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

      Changing the values in the knot vector changes how much each point influences the total curvature (with some clever knot value manipulation, we can even make the influence of certain points disappear entirely!), so we can see that while the control points define the hull inside of which we're going to be drawing a curve, it is actually the knot vector that determines the actual shape of the curve inside that hull.

      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!

      @@ -2392,8 +2396,8 @@ 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.

      +

      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 coordinate. However, because B-Splines are an interpolation of curves, not just points, we can't simply make the first and last point the same, we need to link as many points as are necessary to form "a curve" that the spline performs interpolation with. As such, for an order d B-Spline, we need to make the first and last d points the same. This is of course hardly more work than before (simply append points.splice(0,d) to points) but it's important to remember that you need more than just a single point.

      +

      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. don't just happen to have the same x/y values, but really are the same coordinate, so that manipulating one will equally manipulate the other, but programming generally makes this really easy by storing references to points, rather than copies (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:

        @@ -2407,7 +2411,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2420,7 +2424,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2433,7 +2437,7 @@ for(let L = 1; L <= order; L++) { Scripts are disabled. Showing fallback image. - + @@ -2443,11 +2447,11 @@ for(let L = 1; L <= order; L++) {

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

        - +

        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 closer to that point the spline curve will lie, a bit like turning up the gravity of a control pointl, just like for rational Bézier curves.

        + Scripts are disabled. Showing fallback image. - +