1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-30 19:50:01 +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
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 -->
<div class="two-column">
<KnotController ref="interpolation-graph" />
<BSplineGraphic sketch={this.interpolationGraph} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/>
</div>
<graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./interpolation.js">
<!-- weight factors go here, similar to curve fitting sliders -->
</graphics-element>
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.
<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 -->
</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() {
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`);

View File

@@ -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<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() {
@@ -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<knots.length; i++) {
knots[i] = knots[i-1] + 1;
}
const spline = new BSpline(this, points);
spline.knots = knots;
noFill();
setStroke(`black`);

View File

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