1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-18 14:31:24 +02:00

splines + slider refinement

This commit is contained in:
Pomax
2020-09-06 09:08:11 -07:00
parent 9434a71d34
commit 1de1fc9ce3
21 changed files with 324 additions and 258 deletions

View File

@@ -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 ## 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 GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER --> <!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER -->
<div class="two-column"> <graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./interpolation.js">
<KnotController ref="interpolation-graph" /> <!-- weight factors go here, similar to curve fitting sliders -->
<BSplineGraphic sketch={this.interpolationGraph} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/> </graphics-element>
</div>
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. 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 ## 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 ## 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 ## 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.
<graphics-element title="An rational, uniform B-Spline" width="400" height="400" src="rational-uniform.js"> <graphics-element title="A (closed) rational, uniform B-Spline" width="400" height="400" src="rational-uniform.js">
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
</graphics-element> </graphics-element>

View File

@@ -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<n+1+k; i++) {
this.drawN(i, k, pad, (this.width-pad)/(2*(n+2)), this.height-2*pad);
}
},
drawN(i, k, pad, w, h) {
this.stroke(colors[i]);
let knots = this.knots;
this.beginPath();
for (let start=i-1, t=start, step=0.1, end=i+k+1; t<end; t+=step) {
let x = pad + i*w + t*w;
let y = this.height - pad - this.N(i, k, t) * h;
this.vertex(x, y);
}
this.endPath();
},
N(i, k, t) {
let t_i = this.knots[i];
let t_i1 = this.knots[i+1];
let t_ik1 = this.knots[i+k-1];
let t_ik = this.knots[i+k];
if (k===1) {
return (t_i <= t && t <= t_i1) ? 1 : 0;
}
let n1 = t - t_i;
let d1 = t_ik1 - t_i;
let a1 = d1===0? 0: n1/d1;
let n2 = t_ik - t;
let d2 = t_ik - t_i1;
let a2 = d2===0? 0: n2/d2;
let N1 = 0;
if (a1 !== 0) {
let n1v = this.ensureN(i,k-1,t);
N1 = n1v === undefined ? this.N(i,k-1,t) : n1v;
}
let N2 = 0;
if (a2 !== 0) {
let n2v = this.ensureN(i+1,k-1,t);
N2 = n2v === undefined ? this.N(i+1,k-1,t) : n2v;
}
this.cacheN(i,k,t, a1 * N1 + a2 * N2);
return this.cache.N[i][k][t];
},
ensureN(i,k,t) {
if (!this.cache.N) { this.cache.N = []; }
let N = this.cache.N;
if (!N[i]) { N[i] = []; }
if (!N[i][k]) { N[i][k] = []; }
return N[i][k][t];
},
cacheN(i,k,t,value) {
this.ensureN(i,k,t);
this.cache.N[i][k][t] = value;
}
};

View File

@@ -0,0 +1,122 @@
let cache = { N: [] },
points,
spline,
pad=20,
knots,
colors = [
'#C00',
'#CC0',
'#0C0',
'#0CC',
'#00C',
'#C0C',
'#600',
'#660',
'#060',
'#066',
'#006',
'#606'
];
setup() {
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}
];
knots = new BSpline(this, points).formKnots();
let min = 0, max = knots.length-1;
knots.forEach((_,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.length-1 && v > 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<e; i++) {
this.drawN(i, degree, pad, (this.width-pad)/(2*(n+2)), this.height-2*pad);
}
}
drawN(i, k, pad, w, h) {
noFill();
setStroke(colors[i]);
setWidth(2);
start()
for (let start=i-1, t=start, step=0.1, end=i+k+1; t<end; t+=step) {
let x = pad + i*w + t*w;
let y = this.height - pad - this.N(i, k, t) * h;
vertex(x, y);
}
end();
}
N(i, k, t) {
let t_i = knots[i];
let t_i1 = knots[i+1];
let t_ik1 = knots[i+k-1];
let t_ik = knots[i+k];
if (k===1) {
return (t_i <= t && t <= t_i1) ? 1 : 0;
}
let n1 = t - t_i;
let d1 = t_ik1 - t_i;
let a1 = d1===0? 0: n1/d1;
let n2 = t_ik - t;
let d2 = t_ik - t_i1;
let a2 = d2===0? 0: n2/d2;
let N1 = 0;
if (a1 !== 0) {
let n1v = this.ensureN(i,k-1,t);
N1 = n1v === undefined ? this.N(i,k-1,t) : n1v;
}
let N2 = 0;
if (a2 !== 0) {
let n2v = this.ensureN(i+1,k-1,t);
N2 = n2v === undefined ? this.N(i+1,k-1,t) : n2v;
}
this.cacheN(i,k,t, a1 * N1 + a2 * N2);
return cache.N[i][k][t];
}
ensureN(i,k,t) {
if (!cache.N) { cache.N = []; }
let N = cache.N;
if (!N[i]) { N[i] = []; }
if (!N[i][k]) { N[i][k] = []; }
return N[i][k][t];
}
cacheN(i,k,t,value) {
this.ensureN(i,k,t);
cache.N[i][k][t] = value;
}

View File

@@ -1,42 +0,0 @@
module.exports = {
degree: 3,
activeDistance: 9,
weights: [],
setup() {
this.size(400, 400);
var TAU = Math.PI*2;
for (let i=0; i<TAU; i+=TAU/10) {
this.points.push({
x: this.width/2 + 100 * Math.cos(i),
y: this.height/2 + 100 * Math.sin(i)
});
}
this.knots = this.formKnots(this.points, true);
this.weights = this.formWeights(this.points);
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);
}
};

View File

@@ -1,4 +1,4 @@
let points=[]; let points=[], weights;
setup() { setup() {
var r = this.width/3; var r = this.width/3;
@@ -8,10 +8,21 @@ setup() {
y: this.height/2 + r * Math.sin(i/6 * TAU) 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)); points = points.concat(points.slice(0,3));
setMovable(points); setMovable(points);
} }
setWeight(i, v) {
weights[i] = v;
}
draw() { draw() {
clear(); clear();
@@ -33,7 +44,7 @@ draw() {
drawSplineData() { drawSplineData() {
const spline = new BSpline(this, points, !!this.parameters.open); const spline = new BSpline(this, points, !!this.parameters.open);
spline.formWeights(); spline.weights = weights;
noFill(); noFill();
setStroke(`black`); setStroke(`black`);

View File

@@ -8,6 +8,27 @@ setup() {
}); });
} }
setMovable(points); 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<knots.length; i++) {
knots[i] = knots[i-1] + 1;
}
let min=0, max=knots.length-1;
knots.forEach((_,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.length-1 && v > knots[i+1]) throw {value: knots[i+1]};
knots[i] = v;
redraw();
} }
draw() { draw() {
@@ -30,16 +51,8 @@ draw() {
} }
drawSplineData() { drawSplineData() {
const spline = new BSpline(this, points, !!this.parameters.open); const spline = new BSpline(this, points);
spline.knots = knots;
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<knots.length; i++) {
knots[i] = knots[i-1] + 1;
}
noFill(); noFill();
setStroke(`black`); setStroke(`black`);

View File

@@ -1,4 +1,4 @@
let points=[]; let points=[], knots;
setup() { setup() {
for (let s=TAU/9, i=s/2; i<TAU; i+=s) { for (let s=TAU/9, i=s/2; i<TAU; i+=s) {
@@ -8,6 +8,19 @@ setup() {
}); });
} }
setMovable(points); setMovable(points);
knots = new BSpline(this, points).formKnots(!!this.parameters.open);
let min=0, max=knots.length-1;
knots.forEach((_,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.length-1 && v > knots[i+1]) throw {value: knots[i+1]};
knots[i] = v;
redraw();
} }
draw() { draw() {
@@ -31,7 +44,7 @@ draw() {
drawSplineData() { drawSplineData() {
const spline = new BSpline(this, points); const spline = new BSpline(this, points);
spline.formKnots(!!this.parameters.open); spline.knots = knots;
noFill(); noFill();
setStroke(`black`); setStroke(`black`);

View File

@@ -1,16 +1,15 @@
let points = [], curve, sliders; let points=[], curve, tvalues=[];
setup() { setup() {
let btn = find(`.toggle`); let btn = find(`.toggle`);
if (btn) btn.listen(`click`, evt => this.toggle()); if (btn) btn.listen(`click`, evt => this.toggle());
sliders = find(`.sliders`);
this.mode = 0; this.mode = 0;
this.label = `Using equidistant t values`; this.label = `Using equidistant t values`;
} }
toggle() { toggle() {
this.mode = (this.mode + 1) % 2; this.mode = (this.mode + 1) % 2;
if (sliders) this.setSliderValues(this.mode); this.setSliderValues(this.mode);
redraw(); redraw();
} }
@@ -23,7 +22,7 @@ draw() {
setFontSize(16); setFontSize(16);
setTextStroke(`white`, 4); setTextStroke(`white`, 4);
const n = points.length; const n = points.length;
if (n > 2 && sliders && sliders.values) { if (n > 2) {
curve = this.fitCurveToPoints(n); curve = this.fitCurveToPoints(n);
curve.drawSkeleton(`blue`); curve.drawSkeleton(`blue`);
curve.drawCurve(); curve.drawCurve();
@@ -35,7 +34,7 @@ draw() {
fitCurveToPoints(n) { fitCurveToPoints(n) {
// alright, let's do this thing: // alright, let's do this thing:
const tm = this.formTMatrix(sliders.values, n), const tm = this.formTMatrix(tvalues, n),
T = tm.T, T = tm.T,
Tt = tm.Tt, Tt = tm.Tt,
M = this.generateBasisMatrix(n), M = this.generateBasisMatrix(n),
@@ -107,28 +106,20 @@ onMouseDown() {
updateSliders() { updateSliders() {
if (sliders && points.length > 2) { removeSliders();
sliders.innerHTML = ``; const l = points.length-1;
sliders.values = []; if (l >= 2) {
this.sliders = points.map((p,i) => { points.forEach((_,i) => {
// TODO: this should probably be built into the graphics API as a addSlider(`slide-control`, false, 0, 1, 0.01, i/l, v => this.setTvalue(i, v));
// 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);
}); });
} }
this.label = `Using equidistant t values`;
}
setTvalue(i, t) {
this.label = `Using custom t values`;
tvalues[i] = t;
redraw();
} }
setSliderValues(mode) { setSliderValues(mode) {
@@ -137,7 +128,7 @@ setSliderValues(mode) {
// equidistant // equidistant
if (mode === 0) { if (mode === 0) {
this.label = `Using equidistant t values`; 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 // polygonal distance
@@ -152,11 +143,11 @@ setSliderValues(mode) {
} }
const S = [], len = D[n-1]; const S = [], len = D[n-1];
D.forEach((v,i) => { S[i] = v/len; }); D.forEach((v,i) => { S[i] = v/len; });
sliders.values = S; tvalues = S;
} }
findAll(`.sliders input[type=range]`).forEach((s,i) => { findAll(`input[type=range]`).forEach((s,i) => {
s.setAttribute(`value`, sliders.values[i]); s.setAttribute(`value`, tvalues[i]);
s.value = sliders.values[i]; s.value = tvalues[i];
}); });
} }

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1847,7 +1847,7 @@ for (coordinate, index) in LUT:
<graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" > <graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="550px" height="275px" src="images\chapters\curvefitting\c6c8442e24793ce72a872ce29b2b4125.png"> <img width="550px" height="275px" src="images\chapters\curvefitting\78d32beb061391c47217611128446146.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<button class="toggle">toggle</button> <button class="toggle">toggle</button>
@@ -2373,13 +2373,17 @@ Doing so for a degree <code>d</code> B-Spline with <code>n</code> control point
<p>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 <code>d(..., k)</code> 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!</p> <p>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 <code>d(..., k)</code> 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!</p>
<p>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!</p> <p>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!</p>
<h2>Cool, cool... but I don't know what to do with that information</h2> <h2>Cool, cool... but I don't know what to do with that information</h2>
<p>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.</p> <p>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).</p>
<!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER --> <!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER -->
<div class="two-column"> <graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./chapters/bsplines/interpolation.js" >
<KnotController ref="interpolation-graph" /> <fallback-image>
<BSplineGraphic sketch={this.interpolationGraph} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
</div> <img width="600px" height="300px" src="images\chapters\bsplines\84bd623b9d3d8bf01a656f49c17079b8.png">
<label></label>
</fallback-image>
<!-- weight factors go here, similar to curve fitting sliders -->
</graphics-element>
<p>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 <em>shape</em> of the curve inside that hull.</p> <p>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 <em>shape</em> of the curve inside that hull.</p>
<p>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!</p> <p>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!</p>
@@ -2402,8 +2406,8 @@ for(let L = 1; L &lt;= order; L++) {
}</code></pre> }</code></pre>
<p>(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)</p> <p>(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)</p>
<h2>Open vs. closed paths</h2> <h2>Open vs. closed paths</h2>
<p>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 <em>the same point</em>. 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 <code>d</code> B-Spline, we need to make the last <code>d</code> point the same as the first <code>d</code> points. And the easiest way to do this is to simply append <code>points.splice(0,d)</code> to <code>points</code>. Done!</p> <p>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 <code>d</code> B-Spline, we need to make the first and last <code>d</code> points the same. This is of course hardly more work than before (simply append <code>points.splice(0,d)</code> to <code>points</code>) but it's important to remember that you need more than just a single point.</p>
<p>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 <code>points[0]</code> and <code>points[n-k]</code> 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.</p> <p>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 <code>points[0]</code> and <code>points[n-k]</code> 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.</p>
<h2>Manipulating the curve through the knot vector</h2> <h2>Manipulating the curve through the knot vector</h2>
<p>The most important thing to understand when it comes to B-Splines is that they work <em>because</em> 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 <em>values</em> 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:</p> <p>The most important thing to understand when it comes to B-Splines is that they work <em>because</em> 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 <em>values</em> 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:</p>
<ol> <ol>
@@ -2417,7 +2421,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="A uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" > <graphics-element title="A uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\5d3b04c3161a3429ce651bb7a5fa0399.png"> <img width="400px" height="400px" src="images\chapters\bsplines\7ac6e46280de14dce141fb073777dc8e.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2430,7 +2434,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./chapters/bsplines/reduced.js" > <graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./chapters/bsplines/reduced.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\93146ea89bb21999d9e18b57dd1bdd29.png"> <img width="400px" height="400px" src="images\chapters\bsplines\984c86b3b357c799aaab5a5fbd48d599.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2443,7 +2447,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" data-open="true"> <graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" data-open="true">
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\8caa3e8ff614ad9731b15dacaba98c3c.png"> <img width="400px" height="400px" src="images\chapters\bsplines\4c71f82901edc1a0acae31cf5be12c65.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2453,11 +2457,11 @@ for(let L = 1; L &lt;= order; L++) {
<h3>Non-uniform B-Splines</h3> <h3>Non-uniform B-Splines</h3>
<p>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 <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.</p> <p>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 <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.</p>
<h2>One last thing: Rational B-Splines</h2> <h2>One last thing: Rational B-Splines</h2>
<p>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.</p> <p>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.</p>
<graphics-element title="An rational, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/rational-uniform.js" > <graphics-element title="A (closed) rational, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/rational-uniform.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\fc654445500dd595d6ae9de27a3dc46c.png"> <img width="400px" height="400px" src="images\chapters\bsplines\9915d887443459415a7bfb57bea1b898.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->

View File

@@ -1843,7 +1843,7 @@ for (coordinate, index) in LUT:
<graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" > <graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="550px" height="275px" src="images\chapters\curvefitting\c6c8442e24793ce72a872ce29b2b4125.png"> <img width="550px" height="275px" src="images\chapters\curvefitting\78d32beb061391c47217611128446146.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<button class="toggle">toggle</button> <button class="toggle">toggle</button>
@@ -2369,13 +2369,17 @@ Doing so for a degree <code>d</code> B-Spline with <code>n</code> control point
<p>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 <code>d(..., k)</code> 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!</p> <p>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 <code>d(..., k)</code> 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!</p>
<p>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!</p> <p>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!</p>
<h2>Cool, cool... but I don't know what to do with that information</h2> <h2>Cool, cool... but I don't know what to do with that information</h2>
<p>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.</p> <p>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).</p>
<!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER --> <!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER -->
<div class="two-column"> <graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./chapters/bsplines/interpolation.js" >
<KnotController ref="interpolation-graph" /> <fallback-image>
<BSplineGraphic sketch={this.interpolationGraph} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
</div> <img width="600px" height="300px" src="images\chapters\bsplines\84bd623b9d3d8bf01a656f49c17079b8.png">
<label></label>
</fallback-image>
<!-- weight factors go here, similar to curve fitting sliders -->
</graphics-element>
<p>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 <em>shape</em> of the curve inside that hull.</p> <p>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 <em>shape</em> of the curve inside that hull.</p>
<p>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!</p> <p>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!</p>
@@ -2398,8 +2402,8 @@ for(let L = 1; L &lt;= order; L++) {
}</code></pre> }</code></pre>
<p>(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)</p> <p>(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)</p>
<h2>Open vs. closed paths</h2> <h2>Open vs. closed paths</h2>
<p>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 <em>the same point</em>. 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 <code>d</code> B-Spline, we need to make the last <code>d</code> point the same as the first <code>d</code> points. And the easiest way to do this is to simply append <code>points.splice(0,d)</code> to <code>points</code>. Done!</p> <p>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 <code>d</code> B-Spline, we need to make the first and last <code>d</code> points the same. This is of course hardly more work than before (simply append <code>points.splice(0,d)</code> to <code>points</code>) but it's important to remember that you need more than just a single point.</p>
<p>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 <code>points[0]</code> and <code>points[n-k]</code> 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.</p> <p>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 <code>points[0]</code> and <code>points[n-k]</code> 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.</p>
<h2>Manipulating the curve through the knot vector</h2> <h2>Manipulating the curve through the knot vector</h2>
<p>The most important thing to understand when it comes to B-Splines is that they work <em>because</em> 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 <em>values</em> 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:</p> <p>The most important thing to understand when it comes to B-Splines is that they work <em>because</em> 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 <em>values</em> 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:</p>
<ol> <ol>
@@ -2413,7 +2417,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="A uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" > <graphics-element title="A uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\5d3b04c3161a3429ce651bb7a5fa0399.png"> <img width="400px" height="400px" src="images\chapters\bsplines\7ac6e46280de14dce141fb073777dc8e.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2426,7 +2430,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./chapters/bsplines/reduced.js" > <graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./chapters/bsplines/reduced.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\93146ea89bb21999d9e18b57dd1bdd29.png"> <img width="400px" height="400px" src="images\chapters\bsplines\984c86b3b357c799aaab5a5fbd48d599.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2439,7 +2443,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" data-open="true"> <graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" data-open="true">
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\8caa3e8ff614ad9731b15dacaba98c3c.png"> <img width="400px" height="400px" src="images\chapters\bsplines\4c71f82901edc1a0acae31cf5be12c65.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2449,11 +2453,11 @@ for(let L = 1; L &lt;= order; L++) {
<h3>Non-uniform B-Splines</h3> <h3>Non-uniform B-Splines</h3>
<p>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 <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.</p> <p>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 <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.</p>
<h2>One last thing: Rational B-Splines</h2> <h2>One last thing: Rational B-Splines</h2>
<p>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.</p> <p>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.</p>
<graphics-element title="An rational, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/rational-uniform.js" > <graphics-element title="A (closed) rational, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/rational-uniform.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\fc654445500dd595d6ae9de27a3dc46c.png"> <img width="400px" height="400px" src="images\chapters\bsplines\9915d887443459415a7bfb57bea1b898.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->

View File

@@ -1,4 +1,5 @@
import { enrich } from "../lib/enrich.js"; import { enrich } from "../lib/enrich.js";
import { create } from "../lib/create.js";
import { Bezier } from "./types/bezier.js"; import { Bezier } from "./types/bezier.js";
import { BSpline } from "./types/bspline.js"; import { BSpline } from "./types/bspline.js";
import { Vector } from "./types/vector.js"; import { Vector } from "./types/vector.js";
@@ -176,6 +177,23 @@ class GraphicsAPI extends BaseAPI {
this.line(0, 0, 0, this.height); 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. * 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. * @param {boolean} redraw whether or not to redraw after updating the value from the slider.
*/ */
setSlider(qs, propname, initial, transform) { 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.`); 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) { if (!slider) {
console.warn(`Warning: no slider found for query selector "${qs}"`); console.warn(`Warning: no slider found for query selector "${qs}"`);
this[propname] = initial; if (propname) this[propname] = initial;
return undefined; return undefined;
} }
const updateProperty = (evt) => { const updateProperty = (evt) => {
let value = parseFloat(slider.value); let value = parseFloat(slider.value);
slider.setAttribute(`value`, value); try {
this[propname] = transform ? transform(value) : value; 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(); if (!this.redrawing) this.redraw();
}; };
@@ -211,6 +238,15 @@ class GraphicsAPI extends BaseAPI {
return slider; 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 * Convert the canvas to an image
*/ */
@@ -413,7 +449,7 @@ class GraphicsAPI extends BaseAPI {
* Set the context lineWidth * Set the context lineWidth
*/ */
setWidth(width) { setWidth(width) {
this.ctx.lineWidth = `${width}px`; this.ctx.lineWidth = width;
} }
/** /**

View File

@@ -1,4 +1,4 @@
import interpolate from "../util/spline.js"; import interpolate from "../util/interpolate-bspline.js";
// cubic B-Spline // cubic B-Spline
const DEGREE = 3; const DEGREE = 3;

View File

@@ -24,6 +24,11 @@ export default function interpolate(
} }
} }
// closed curve?
if (weights.length < points.length) {
weights = weights.concat(weights.slice(0, degree));
}
if (!knots) { if (!knots) {
// build knot vector of length [n + degree + 1] // build knot vector of length [n + degree + 1]
var knots = []; var knots = [];
@@ -35,6 +40,11 @@ export default function interpolate(
throw new Error("bad knot vector length"); 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 domain = [degree, knots.length - 1 - degree];
var low = knots[domain[0]]; var low = knots[domain[0]];

View File

@@ -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 };

View File

@@ -1837,7 +1837,7 @@ for (coordinate, index) in LUT:
<graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" > <graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="550px" height="275px" src="images\chapters\curvefitting\c6c8442e24793ce72a872ce29b2b4125.png"> <img width="550px" height="275px" src="images\chapters\curvefitting\78d32beb061391c47217611128446146.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<button class="toggle">toggle</button> <button class="toggle">toggle</button>
@@ -2363,13 +2363,17 @@ Doing so for a degree <code>d</code> B-Spline with <code>n</code> control point
<p>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 <code>d(..., k)</code> 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!</p> <p>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 <code>d(..., k)</code> 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!</p>
<p>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!</p> <p>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!</p>
<h2>Cool, cool... but I don't know what to do with that information</h2> <h2>Cool, cool... but I don't know what to do with that information</h2>
<p>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.</p> <p>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).</p>
<!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER --> <!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER -->
<div class="two-column"> <graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./chapters/bsplines/interpolation.js" >
<KnotController ref="interpolation-graph" /> <fallback-image>
<BSplineGraphic sketch={this.interpolationGraph} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
</div> <img width="600px" height="300px" src="images\chapters\bsplines\84bd623b9d3d8bf01a656f49c17079b8.png">
<label></label>
</fallback-image>
<!-- weight factors go here, similar to curve fitting sliders -->
</graphics-element>
<p>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 <em>shape</em> of the curve inside that hull.</p> <p>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 <em>shape</em> of the curve inside that hull.</p>
<p>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!</p> <p>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!</p>
@@ -2392,8 +2396,8 @@ for(let L = 1; L &lt;= order; L++) {
}</code></pre> }</code></pre>
<p>(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)</p> <p>(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)</p>
<h2>Open vs. closed paths</h2> <h2>Open vs. closed paths</h2>
<p>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 <em>the same point</em>. 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 <code>d</code> B-Spline, we need to make the last <code>d</code> point the same as the first <code>d</code> points. And the easiest way to do this is to simply append <code>points.splice(0,d)</code> to <code>points</code>. Done!</p> <p>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 <code>d</code> B-Spline, we need to make the first and last <code>d</code> points the same. This is of course hardly more work than before (simply append <code>points.splice(0,d)</code> to <code>points</code>) but it's important to remember that you need more than just a single point.</p>
<p>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 <code>points[0]</code> and <code>points[n-k]</code> 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.</p> <p>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 <code>points[0]</code> and <code>points[n-k]</code> 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.</p>
<h2>Manipulating the curve through the knot vector</h2> <h2>Manipulating the curve through the knot vector</h2>
<p>The most important thing to understand when it comes to B-Splines is that they work <em>because</em> 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 <em>values</em> 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:</p> <p>The most important thing to understand when it comes to B-Splines is that they work <em>because</em> 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 <em>values</em> 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:</p>
<ol> <ol>
@@ -2407,7 +2411,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="A uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" > <graphics-element title="A uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\5d3b04c3161a3429ce651bb7a5fa0399.png"> <img width="400px" height="400px" src="images\chapters\bsplines\7ac6e46280de14dce141fb073777dc8e.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2420,7 +2424,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./chapters/bsplines/reduced.js" > <graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./chapters/bsplines/reduced.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\93146ea89bb21999d9e18b57dd1bdd29.png"> <img width="400px" height="400px" src="images\chapters\bsplines\984c86b3b357c799aaab5a5fbd48d599.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2433,7 +2437,7 @@ for(let L = 1; L &lt;= order; L++) {
<graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" data-open="true"> <graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/uniform.js" data-open="true">
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\8caa3e8ff614ad9731b15dacaba98c3c.png"> <img width="400px" height="400px" src="images\chapters\bsplines\4c71f82901edc1a0acae31cf5be12c65.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->
@@ -2443,11 +2447,11 @@ for(let L = 1; L &lt;= order; L++) {
<h3>Non-uniform B-Splines</h3> <h3>Non-uniform B-Splines</h3>
<p>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 <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.</p> <p>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 <code>knots[k+1]</code> should be greater than or equal to <code>knots[k]</code>.</p>
<h2>One last thing: Rational B-Splines</h2> <h2>One last thing: Rational B-Splines</h2>
<p>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.</p> <p>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.</p>
<graphics-element title="An rational, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/rational-uniform.js" > <graphics-element title="A (closed) rational, uniform B-Spline" width="400" height="400" src="./chapters/bsplines/rational-uniform.js" >
<fallback-image> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span> <span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="400px" height="400px" src="images\chapters\bsplines\fc654445500dd595d6ae9de27a3dc46c.png"> <img width="400px" height="400px" src="images\chapters\bsplines\9915d887443459415a7bfb57bea1b898.png">
<label></label> <label></label>
</fallback-image> </fallback-image>
<!-- knot sliders go here, similar to the curve fitter section --> <!-- knot sliders go here, similar to the curve fitter section -->