mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-22 16:23:12 +02:00
bsplines
This commit is contained in:
@@ -18,6 +18,7 @@ var BSplineGraphic = React.createClass({
|
||||
this.activeDistance = 9;
|
||||
this.points = [];
|
||||
this.knots = [];
|
||||
this.weights = [];
|
||||
this.nodes = [];
|
||||
this.cp = undefined;
|
||||
this.dx = undefined;
|
||||
@@ -37,33 +38,55 @@ var BSplineGraphic = React.createClass({
|
||||
return <canvas className="bspline-graphic" ref="sketch" />;
|
||||
},
|
||||
|
||||
keydownlisten(e) { this.setKeyboardValues(e); this.keyDown(); },
|
||||
keyuplisten(e) { this.setKeyboardValues(e); this.keyUp(); },
|
||||
keypresslisten(e) { this.setKeyboardValues(e); this.keyPressed(); },
|
||||
mousedownlisten(e) { this.setMouseValues(e); this.mouseDown(); },
|
||||
mouseuplisten(e) { this.setMouseValues(e); this.mouseUp(); },
|
||||
mousemovelisten(e) { this.setMouseValues(e); this.mouseMove(); if(this.isMouseDown && this.mouseDrag) { this.mouseDrag(); }},
|
||||
wheellissten(e) { e.preventDefault(); this.scrolled(e.deltaY < 0 ? 1 : -1); },
|
||||
|
||||
componentDidMount() {
|
||||
var cvs = this.cvs = this.refs.sketch;
|
||||
// Keyboard event handling
|
||||
cvs.addEventListener("keydown", (e) => { this.setKeyboardValues(e); if (typeof this.keyDown !== "undefined") { this.keyDown(); }});
|
||||
cvs.addEventListener("keyup", (e) => { this.setKeyboardValues(e); if (typeof this.keyUp !== "undefined") { this.keyUp(); }});
|
||||
cvs.addEventListener("keypress", (e) => { this.setKeyboardValues(e); if (typeof this.keyPressed !== "undefined") { this.keyPressed(); }});
|
||||
cvs.addEventListener("keydown", this.keydownlisten);
|
||||
cvs.addEventListener("keyup", this.keyuplisten);
|
||||
cvs.addEventListener("keypress", this.keypresslisten);
|
||||
// Mouse event handling
|
||||
cvs.addEventListener("mousedown", (e) => { this.setMouseValues(e); if (typeof this.mouseDown !== "undefined") { this.mouseDown(); }});
|
||||
cvs.addEventListener("mouseup", (e) => { this.setMouseValues(e); if (typeof this.mouseUp !== "undefined") { this.mouseUp(); }});
|
||||
cvs.addEventListener("mousemove", (e) => { this.setMouseValues(e); if (typeof this.mouseMove !== "undefined") { this.mouseMove(); } if(this.isMouseDown && this.mouseDrag) { this.mouseDrag(); }});
|
||||
cvs.addEventListener("mousedown", this.mousedownlisten);
|
||||
cvs.addEventListener("mouseup", this.mouseuplisten);
|
||||
cvs.addEventListener("mousemove", this.mousemovelisten);
|
||||
// Scroll event handling
|
||||
if (this.props.scrolling) { cvs.addEventListener("wheel", this.wheellissten); }
|
||||
// Boom let's go
|
||||
this.setup();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
var cvs = this.cvs = this.refs.sketch;
|
||||
cvs.removeEventListener("keydown", this.keydownlisten);
|
||||
cvs.removeEventListener("keyup", this.keyuplisten);
|
||||
cvs.removeEventListener("keypress", this.keypresslisten);
|
||||
cvs.removeEventListener("mousedown", this.mousedownlisten);
|
||||
cvs.removeEventListener("mouseup", this.mouseuplisten);
|
||||
cvs.removeEventListener("mousemove", this.mousemovelisten);
|
||||
if (this.props.scrolling) { cvs.removeEventListener("wheel", this.wheellissten); }
|
||||
},
|
||||
|
||||
// base API
|
||||
|
||||
drawCurve(points) {
|
||||
points = points || this.points;
|
||||
var ctx = this.ctx;
|
||||
var weights = this.weights.length>0 ? this.weights : false;
|
||||
ctx.beginPath();
|
||||
var p = interpolate(0, this.degree, points, this.knots);
|
||||
var p = interpolate(0, this.degree, points, this.knots, weights);
|
||||
ctx.moveTo(p[0], p[1]);
|
||||
for(let t=0.01; t<1; t+=0.01) {
|
||||
p = interpolate(t, this.degree, points, this.knots);
|
||||
p = interpolate(t, this.degree, points, this.knots, weights);
|
||||
ctx.lineTo(p[0], p[1]);
|
||||
}
|
||||
p = interpolate(1, this.degree, points, this.knots);
|
||||
p = interpolate(1, this.degree, points, this.knots, weights);
|
||||
ctx.lineTo(p[0], p[1]);
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
@@ -71,11 +94,12 @@ var BSplineGraphic = React.createClass({
|
||||
|
||||
drawKnots(points) {
|
||||
var knots = this.knots;
|
||||
var weights = this.weights.length>0 ? this.weights : false;
|
||||
knots.forEach((knot,i) => {
|
||||
if (i < this.degree) return;
|
||||
if (i > knots.length - 1 - this.degree) return;
|
||||
var p = interpolate(knot, this.degree, points, knots, false, false, true);
|
||||
this.ellipse(p[0], p[1], 3);
|
||||
var p = interpolate(knot, this.degree, points, knots, weights, false, true);
|
||||
this.circle(p[0], p[1], 3);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -102,11 +126,11 @@ var BSplineGraphic = React.createClass({
|
||||
i;
|
||||
|
||||
// form the open-uniform knot vector
|
||||
for (i=1; i < l - this.degree; i++) { knots.push(i); }
|
||||
for (i=1; i < l - this.degree; i++) { knots.push(i + this.degree); }
|
||||
// add [degree] zeroes at the front
|
||||
for (i=0; i <= this.degree; i++) { knots = [0].concat(knots); }
|
||||
for (i=0; i <= this.degree; i++) { knots = [this.degree].concat(knots); }
|
||||
// add [degree] max-values to the back
|
||||
for (i=0; i <= this.degree; i++) { knots.push(m); }
|
||||
for (i=0; i <= this.degree; i++) { knots.push(m + this.degree); }
|
||||
|
||||
return knots;
|
||||
},
|
||||
@@ -138,6 +162,12 @@ var BSplineGraphic = React.createClass({
|
||||
return nodes;
|
||||
},
|
||||
|
||||
formWeights(points) {
|
||||
var weights = [];
|
||||
points.forEach(p => weights.push(1));
|
||||
return weights;
|
||||
},
|
||||
|
||||
setDegree(d) {
|
||||
this.degree += d;
|
||||
this.knots = this.formKnots(this.points);
|
||||
@@ -177,7 +207,7 @@ var BSplineGraphic = React.createClass({
|
||||
keyUp() {
|
||||
// ... do nothing?
|
||||
},
|
||||
|
||||
|
||||
keyPressed() {
|
||||
// ... do nothing?
|
||||
},
|
||||
@@ -211,6 +241,28 @@ var BSplineGraphic = React.createClass({
|
||||
// ... do nothing?
|
||||
},
|
||||
|
||||
scrolled(direction) {
|
||||
this.cp = this.getCurrentPoint(this.mouseX, this.mouseY);
|
||||
if (!this.cp) return;
|
||||
// base case
|
||||
var pos = this.points.indexOf(this.cp);
|
||||
if (this.weights.length>pos) {
|
||||
this.weights[pos] += direction * 0.1;
|
||||
if (this.weights[pos] < 0) {
|
||||
this.weights[pos] = 0;
|
||||
}
|
||||
}
|
||||
// possible multiplicity due to "closed" curves
|
||||
pos = this.points.indexOf(this.cp, pos+1);
|
||||
if (pos!==-1 && this.weights.length>pos) {
|
||||
this.weights[pos] += direction * 0.1;
|
||||
if (this.weights[pos] < 0) {
|
||||
this.weights[pos] = 0;
|
||||
}
|
||||
}
|
||||
this.redraw();
|
||||
},
|
||||
|
||||
// keyboard events
|
||||
setKeyboardValues(e) {
|
||||
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
@@ -226,7 +278,7 @@ var BSplineGraphic = React.createClass({
|
||||
this.mouseX = e.clientX - brect.left;
|
||||
this.mouseY = e.clientY - brect.top;
|
||||
},
|
||||
|
||||
|
||||
// API stuffs
|
||||
|
||||
size(w,h) {
|
||||
@@ -250,7 +302,7 @@ var BSplineGraphic = React.createClass({
|
||||
for(let y=spacing; y<this.height-1; y+=spacing) { this.line(0,y,this.width,y); }
|
||||
},
|
||||
|
||||
ellipse(x,y,r) {
|
||||
circle(x,y,r) {
|
||||
let hr = r/2;
|
||||
var ctx = this.ctx;
|
||||
ctx.beginPath();
|
||||
@@ -270,6 +322,9 @@ var BSplineGraphic = React.createClass({
|
||||
},
|
||||
|
||||
stroke(r,g,b,a) {
|
||||
if (typeof r === "string") {
|
||||
return (this.ctx.strokeStyle = r);
|
||||
}
|
||||
if (g===undefined) { g=r; b=r; }
|
||||
if (a===undefined) { a = 1; }
|
||||
this.ctx.strokeStyle = this.rgba(r,g,b,a);
|
||||
@@ -278,6 +333,9 @@ var BSplineGraphic = React.createClass({
|
||||
noStroke() { this.ctx.strokeStyle = "none"; },
|
||||
|
||||
fill(r,g,b,a) {
|
||||
if (typeof r === "string") {
|
||||
return (this.ctx.fillStyle = r);
|
||||
}
|
||||
if (g===undefined) { g=r; b=r; }
|
||||
if (a===undefined) { a = 1; }
|
||||
this.ctx.fillStyle = this.rgba(r,g,b,a);
|
||||
@@ -290,7 +348,7 @@ var BSplineGraphic = React.createClass({
|
||||
beginPath() {
|
||||
this.ctx.beginPath(); this.bp = true;
|
||||
},
|
||||
|
||||
|
||||
vertex(x,y) {
|
||||
if (!this.bp) {
|
||||
return this.ctx.lineTo(x,y);
|
||||
|
39
components/KnotController.jsx
Normal file
39
components/KnotController.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
var React = require('react');
|
||||
|
||||
var KnotController = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
owner: false,
|
||||
knots: []
|
||||
};
|
||||
},
|
||||
bindKnots(owner, knots) {
|
||||
this.setState({owner, knots});
|
||||
},
|
||||
render() {
|
||||
var type = 'range';
|
||||
var min = 0;
|
||||
var max = this.state.knots.length;
|
||||
var step = 1;
|
||||
return (
|
||||
<section className='knot-controls'><h2>knot values</h2>{
|
||||
this.state.knots.map((value,position) => {
|
||||
var props = {
|
||||
type, min, max, step,
|
||||
value,
|
||||
onChange: e => {
|
||||
var k = this.state.knots;
|
||||
k[position] = e.target.value;
|
||||
this.setState({ knots: k }, () => {
|
||||
this.state.owner.redraw();
|
||||
});
|
||||
}
|
||||
};
|
||||
return <div key={'knot'+position}>{min}<input {...props}/>{max} (= {value})</div>;
|
||||
})
|
||||
}</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = KnotController;
|
53
components/WeightController.jsx
Normal file
53
components/WeightController.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
var React = require('react');
|
||||
|
||||
var WeightController = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
owner: false,
|
||||
weights: [],
|
||||
closed: false
|
||||
};
|
||||
},
|
||||
bindWeights(owner, weights, closed) {
|
||||
this.setState({owner, weights, closed});
|
||||
},
|
||||
render() {
|
||||
var type = 'range';
|
||||
var min = 0;
|
||||
var max = this.state.weights.length;
|
||||
var step = 1;
|
||||
|
||||
var overlap = this.state.closed;
|
||||
var baselength = this.state.weights.length;
|
||||
if (overlap !== false) {
|
||||
baselength -= overlap;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='knot-controls'><h2>weight values</h2>{
|
||||
this.state.weights.map((value,position) => {
|
||||
if (overlap && position >= baselength) {
|
||||
return null;
|
||||
}
|
||||
var props = {
|
||||
type, min, max, step,
|
||||
value,
|
||||
onChange: e => {
|
||||
var k = this.state.weights;
|
||||
k[position] = e.target.value;
|
||||
if (overlap && position < overlap) {
|
||||
k[position+baselength] = e.target.value;
|
||||
}
|
||||
this.setState({ weights: k }, () => {
|
||||
this.state.owner.redraw();
|
||||
});
|
||||
}
|
||||
};
|
||||
return <div key={'knot'+position}>{min}<input {...props}/>{max} (= {value})</div>;
|
||||
})
|
||||
}</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = WeightController;
|
@@ -16,7 +16,7 @@ module.exports = {
|
||||
this.line(n.x, n.y, p.x, p.y);
|
||||
p = n;
|
||||
this.stroke(0);
|
||||
this.ellipse(p.x, p.y, 4);
|
||||
this.circle(p.x, p.y, 4);
|
||||
});
|
||||
this.drawSplineData();
|
||||
},
|
||||
|
52
components/sections/bsplines/center-cut-bspline.js
Normal file
52
components/sections/bsplines/center-cut-bspline.js
Normal file
@@ -0,0 +1,52 @@
|
||||
module.exports = {
|
||||
degree: 3,
|
||||
activeDistance: 9,
|
||||
|
||||
setup() {
|
||||
this.size(400, 400);
|
||||
|
||||
var TAU = Math.PI*2;
|
||||
for (let i=0; i<TAU; i+=TAU/9) {
|
||||
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);
|
||||
var m = Math.round(this.points.length/2)|0;
|
||||
this.knots[m+0] = this.knots[m];
|
||||
this.knots[m+1] = this.knots[m];
|
||||
this.knots[m+2] = this.knots[m];
|
||||
for (let i=m+3; i<this.knots.length; i++) {
|
||||
this.knots[i] = this.knots[i-1] + 1;
|
||||
}
|
||||
|
||||
if(this.props.controller) {
|
||||
this.props.controller(this, this.knots);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
@@ -1,6 +1,8 @@
|
||||
var React = require("react");
|
||||
var BSplineGraphic = require("../../BSplineGraphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
var KnotController = require("../../KnotController.jsx");
|
||||
var WeightController = require("../../WeightController.jsx");
|
||||
|
||||
var BoundingBox = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
@@ -9,6 +11,14 @@ var BoundingBox = React.createClass({
|
||||
};
|
||||
},
|
||||
|
||||
bindKnots: function(owner, knots, ref) {
|
||||
this.refs[ref].bindKnots(owner, knots);
|
||||
},
|
||||
|
||||
bindWeights: function(owner, weights, closed, ref) {
|
||||
this.refs[ref].bindWeights(owner, weights, closed);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
@@ -70,11 +80,11 @@ var BoundingBox = React.createClass({
|
||||
point on the curve for some value <code>t</code> in the interval [0,1] (where 0 is the start
|
||||
of the curve, and 1 the end, just like for Bezier curves), by evaluting the following function:
|
||||
</p>
|
||||
|
||||
|
||||
<p>\[
|
||||
Point(t) = \sum^n_{i=0} P_i \cdot N_{i,k}(t)
|
||||
\]</p>
|
||||
|
||||
|
||||
<p>
|
||||
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-spline curve
|
||||
is defined as "a mix of all the control points, weighted somehow", where the weighting is achieved
|
||||
@@ -97,7 +107,7 @@ var BoundingBox = React.createClass({
|
||||
<p>\[
|
||||
N_{i,k}(t) = \left ( \frac{t-knot_i}{knot_{(i+k-1)} - knot_i}\right ) \cdot N_{i,k-1}(t) + \left ( \frac{knot_{(i+k)}-t}{knot_{(i+k)} - knot_{(i+1)}} \right ) \cdot N_{i+1,k-1}(t)
|
||||
\]</p>
|
||||
|
||||
|
||||
<p>
|
||||
So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation,
|
||||
on a specific knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that is a
|
||||
@@ -105,10 +115,10 @@ var BoundingBox = React.createClass({
|
||||
expect that this recursion has to stop at some point; obviously, it does, and specifically it does so for
|
||||
the following <code>i</code>/<code>k</code> values:
|
||||
</p>
|
||||
|
||||
|
||||
<p>\[
|
||||
N_{i,1}(t) = \left\{\begin{matrix}
|
||||
1& \text{if } t \in [knot_i,knot_{i+1}) \\
|
||||
1& \text{if } t \in [knot_i,knot_{i+1}) \\
|
||||
0& \text{otherwise}
|
||||
\end{matrix}\right.
|
||||
\]</p>
|
||||
@@ -120,13 +130,13 @@ var BoundingBox = React.createClass({
|
||||
bounded by <code>knots[d]</code> and <code>knots[n]</code>, which are the start point and end point where curvature
|
||||
is controlled by exactly <code>order</code> control points. For instance, for degree 3 (=order 4) and 7 control points,
|
||||
with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map <code>t</code> from [the interval 0,1] to the interval [4,8],
|
||||
and then use that value in the functions above, instead.
|
||||
and then use that value in the functions above, instead.
|
||||
</p>
|
||||
|
||||
|
||||
<h2>
|
||||
Can we simplify that?
|
||||
</h2>
|
||||
|
||||
|
||||
<p>
|
||||
We can, yes.
|
||||
</p>
|
||||
@@ -136,18 +146,18 @@ var BoundingBox = React.createClass({
|
||||
to a mathematically pleasing solution: to compute a point P(t), we can compute this point by
|
||||
evaluating <em>d(t)</em> on a curve section between knots <em>i</em> and <em>i+1</em>:
|
||||
</p>
|
||||
|
||||
|
||||
<p>\[
|
||||
d^k_i(t) = \alpha_{i,k} \cdot d^{k-1}_i(t) + (1-\alpha_{i,k}) \cdot d^{k-1}_{i-1}(t)
|
||||
\]</p>
|
||||
|
||||
|
||||
This is another recursive function, with <em>k</em> values decreasing from the curve order to 1,
|
||||
and the value <em>α</em> (alpha) defined by:
|
||||
|
||||
<p>\[
|
||||
\alpha_{i,k} = \frac{t - knots[i]}{knots[i+1+n-k] - knots[i]}
|
||||
\]</p>
|
||||
|
||||
|
||||
<p>
|
||||
That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers
|
||||
and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha".
|
||||
@@ -156,25 +166,25 @@ var BoundingBox = React.createClass({
|
||||
is also computationally cheap because each step only involves very simple maths. Of course as before
|
||||
the recursion has to stop:
|
||||
</p>
|
||||
|
||||
|
||||
<p>\[
|
||||
d^k_0(t) = 0, \ d^0_i(t) = N_{i,1}(t) =
|
||||
d^k_0(t) = 0, \ d^0_i(t) = N_{i,1}(t) =
|
||||
\left\{\begin{matrix}
|
||||
1& \text{if } t \in [knot_i,knot_{i+1}) \\
|
||||
1& \text{if } t \in [knot_i,knot_{i+1}) \\
|
||||
0& \text{otherwise}
|
||||
\end{matrix}\right.
|
||||
\]</p>
|
||||
|
||||
|
||||
<p>
|
||||
So, we see two stopping conditions: either <code>i</code> becomes 0, in which case d() is zero,
|
||||
or <code>k</code> becomes zero, in which case we get the same "either 1 or 0" that we saw in the N()
|
||||
function above.</p>
|
||||
|
||||
<p>
|
||||
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute
|
||||
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute
|
||||
a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:
|
||||
</p>
|
||||
|
||||
|
||||
<p>\[\begin{array}{ccccccc}
|
||||
d^3_3 &→& d^2_3 &→& d^1_3 &→& d^0_3 (= 0 \text{ or } 1) \\
|
||||
&+^{α^3_3 \times …}_{(1-{α^3_3}) \times …}& &+^{α^2_3 \times …}_{(1-{α^2_3}) \times …}& &+^{α^1_3 \times …}_{(1-{α^1_3}) \times …}&\\
|
||||
@@ -192,7 +202,7 @@ var BoundingBox = React.createClass({
|
||||
That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and
|
||||
we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
One thing we need to keep in mind is that we're working with a spline that is contrained by its control points,
|
||||
so even though the \(d^k_i\) values are zero or one at the lowest level, they are really "zero or one, times their
|
||||
@@ -203,7 +213,7 @@ var BoundingBox = React.createClass({
|
||||
|
||||
<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
|
||||
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 multiplcations, 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
|
||||
@@ -213,7 +223,7 @@ var BoundingBox = React.createClass({
|
||||
<h2>
|
||||
Running the computation
|
||||
</h2>
|
||||
|
||||
|
||||
<p>
|
||||
Unlike the de Casteljau algorithm, where the <code>t</code> value stays the same at every iteration, for B-Splines that
|
||||
is not the case, and so we end having to (for each point we evaluate) run a fairly involving bit of recursive computation.
|
||||
@@ -221,12 +231,12 @@ var BoundingBox = React.createClass({
|
||||
Tech</a> page, but an easier to read version is implemented
|
||||
by <a href="https://github.com/thibauts/b-spline/blob/master/index.js#L59-L71">b-spline.js</a>, so we'll look at its code.
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
Given an input value <code>t</code>, we first map the input to a value from the domain [0,1] to the domain [knots[degree],
|
||||
knots[knots.length - 1 - degree]. Then, we find the section number <code>s</code> that this mapped <code>t</code> value lies on:
|
||||
</p>
|
||||
|
||||
|
||||
<pre>
|
||||
for(s=domain[0]; s < domain[1]; s++) {
|
||||
if(knots[s] <= t && t <= knots[s+1]) break;
|
||||
@@ -247,13 +257,25 @@ var BoundingBox = React.createClass({
|
||||
let v[i] = alpha * v[i] + (1-alpha) * v[i-1]
|
||||
}
|
||||
}</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 have an array index that doesn't exist)
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
Open vs. closed paths
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Much like poly-Beziers, 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>
|
||||
|
||||
<h2>
|
||||
Manipulating the curve through the knot vector
|
||||
</h2>
|
||||
@@ -277,14 +299,17 @@ var BoundingBox = React.createClass({
|
||||
<h3>Uniform B-Splines</h3>
|
||||
|
||||
<p>
|
||||
The most straightforward type of B-spline is the uniform spline. In a uniform spline, the knots are distributed
|
||||
The most straightforward type of B-spline is the uniform spline. In a uniform spline, the knots are distributed
|
||||
uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform
|
||||
knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines <em>the same intervals</em>,
|
||||
or even [0,2,3,...,18,20,22], which also defines <em>the same intervals</em>, just scaled by a constant factor,
|
||||
which becomes normalised during interpolation and so does not contribute to the curvature.
|
||||
</p>
|
||||
|
||||
<p>SHOW A UNIFORM SPLINE HERE</p>
|
||||
<div className="two-column">
|
||||
<KnotController ref="uniform-spline" />
|
||||
<BSplineGraphic sketch={require('./uniform-bspline')} controller={(owner, knots) => this.bindKnots(owner, knots, "uniform-spline")}/>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
This is an important point: the intervals that the knot vector defines are <em>relative</em> intervals, so it
|
||||
@@ -308,7 +333,10 @@ var BoundingBox = React.createClass({
|
||||
collapsing <code>order</code> knots creates a situation where all continuity is lost and the curve "kinks".
|
||||
</p>
|
||||
|
||||
<p>SHOW A CENTER-CUT SPLINE HERE</p>
|
||||
<div className="two-column">
|
||||
<KnotController ref="center-cut-bspline" />
|
||||
<BSplineGraphic sketch={require('./center-cut-bspline')} controller={(owner, knots) => this.bindKnots(owner, knots, "center-cut-bspline")}/>
|
||||
</div>
|
||||
|
||||
<h3>Open-Uniform B-splines</h3>
|
||||
|
||||
@@ -325,11 +353,14 @@ var BoundingBox = React.createClass({
|
||||
"identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.
|
||||
</p>
|
||||
|
||||
<p>SHOW AN OPEN-UNIFORM SPLINE HERE</p>
|
||||
<div className="two-column">
|
||||
<KnotController ref="open-uniform-bspline" />
|
||||
<BSplineGraphic sketch={require('./open-uniform-bspline')} controller={(owner, knots) => this.bindKnots(owner, knots, "open-uniform-bspline")}/>
|
||||
</div>
|
||||
|
||||
<h3>Non-uniform B-splines</h3>
|
||||
|
||||
<p>This is essentialy the "free form" version of a B-spline, and also the least interesting to look at,
|
||||
<p>This is essentialy 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 on constraint to the knot vector, and that is that any value <code>knots[k+1]</code>
|
||||
should be equal to, or greater than <code>knots[k]</code>.</p>
|
||||
@@ -343,7 +374,16 @@ var BoundingBox = React.createClass({
|
||||
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>SHOW A WEIGHT-MANIPULABLE RATIONAL B-SPLINE HERE</p>
|
||||
<div className="two-column">
|
||||
{
|
||||
// <KnotController ref="rational-uniform-bspline" />
|
||||
}
|
||||
<WeightController ref="rational-uniform-bspline-weights" />
|
||||
<BSplineGraphic scrolling={true} sketch={require('./rational-uniform-bspline')} controller={(owner, knots, weights, closed) => {
|
||||
// this.bindKnots(owner, knots, "rational-uniform-bspline");
|
||||
this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights");
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<p>Of course this brings us to the final topic that any text on B-splines must touch on before calling it
|
||||
a day: the NURBS, or Non-Uniform Rational B-Spline (NUBRS is not a plural, the capital S actually just stands
|
||||
@@ -352,13 +392,11 @@ var BoundingBox = React.createClass({
|
||||
as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.
|
||||
</p>
|
||||
|
||||
<p>While a true non-uniform rational B-spline would be hard to work with, when we talk about NURBS we
|
||||
<p>While a true non-uniform rational B-spline would be hard to work with, when we talk about NURBS we
|
||||
typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly
|
||||
as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which
|
||||
has the useful property of starting the curve at the first control point, and ending it at the last.</p>
|
||||
|
||||
<p>SHOW A NURBS HERE</p>
|
||||
|
||||
<h2>Extending our implementation to cover rational splines</h2>
|
||||
|
||||
<p>
|
||||
@@ -387,7 +425,7 @@ var BoundingBox = React.createClass({
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Based on our previous example, we take the final 3D point <code>(x', y', w')</code>, which we then
|
||||
Based on our previous example, we take the final 3D point <code>(x', y', w')</code>, which we then
|
||||
turn back into a 2D point by computing <code>(x'/w', y'/w')</code>. And that's it, we're done!
|
||||
</p>
|
||||
|
||||
|
@@ -24,8 +24,9 @@ module.exports = {
|
||||
var nt = n+k+1;
|
||||
var w2 = this.width/2;
|
||||
var h1 = this.height;
|
||||
var step = 0.1;
|
||||
var ti = [0,1,2,3,4,5,6];
|
||||
var step = 0.1, t = ti[0];
|
||||
var t = ti[0];
|
||||
var N = [[],[],[],[],[],[],[],[]];
|
||||
|
||||
var i1 = 0;
|
||||
@@ -39,7 +40,7 @@ module.exports = {
|
||||
}
|
||||
N[i][l] = 1;
|
||||
// basis functions calculation
|
||||
for (var m = 2; m <= k; m++) {
|
||||
for (var m = 2; m <= k; m++) {
|
||||
var jb = i-m+1;
|
||||
if (jb < 0) {
|
||||
jb = 0;
|
||||
@@ -52,10 +53,20 @@ module.exports = {
|
||||
t += step;
|
||||
}
|
||||
|
||||
var stw = this.width/6;
|
||||
var colors = [
|
||||
'#C00',
|
||||
'#CC0',
|
||||
'#0C0',
|
||||
'#0CC',
|
||||
'#00C',
|
||||
'#C0C'
|
||||
];
|
||||
|
||||
var stw = this.width/8;
|
||||
for (let j = 0; j < n1; j++) {
|
||||
t = ti[0];
|
||||
let to = t;
|
||||
this.stroke(colors[j]);
|
||||
for (let l = 1; l < w2; l++) {
|
||||
t += step;
|
||||
let t1 = t;
|
||||
@@ -68,5 +79,11 @@ module.exports = {
|
||||
to = t1;
|
||||
}
|
||||
}
|
||||
|
||||
this.stroke(0);
|
||||
this.fill(0);
|
||||
for(let j=0; j<n+k+1; j++) {
|
||||
this.circle(pad + j*stw, h1 - pad, 3);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
45
components/sections/bsplines/open-uniform-bspline.js
Normal file
45
components/sections/bsplines/open-uniform-bspline.js
Normal file
@@ -0,0 +1,45 @@
|
||||
module.exports = {
|
||||
degree: 3,
|
||||
activeDistance: 9,
|
||||
|
||||
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);
|
||||
|
||||
if(this.props.controller) {
|
||||
this.props.controller(this, this.knots);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
42
components/sections/bsplines/rational-non-uniform-bspline.js
Normal file
42
components/sections/bsplines/rational-non-uniform-bspline.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
}
|
||||
};
|
50
components/sections/bsplines/rational-uniform-bspline.js
Normal file
50
components/sections/bsplines/rational-uniform-bspline.js
Normal file
@@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
degree: 3,
|
||||
activeDistance: 9,
|
||||
weights: [],
|
||||
|
||||
setup() {
|
||||
this.size(400, 400);
|
||||
|
||||
var TAU = Math.PI*2;
|
||||
var r = this.width/3;
|
||||
for (let i=0; i<6; i++) {
|
||||
this.points.push({
|
||||
x: this.width/2 + r * Math.cos(i/6 * TAU),
|
||||
y: this.height/2 + r * Math.sin(i/6 * TAU)
|
||||
});
|
||||
}
|
||||
this.points = this.points.concat(this.points.slice(0,3));
|
||||
this.closed = this.degree;
|
||||
|
||||
this.knots = this.formKnots(this.points);
|
||||
this.weights = this.formWeights(this.points);
|
||||
|
||||
if(this.props.controller) {
|
||||
this.props.controller(this, this.knots, this.weights, this.closed);
|
||||
}
|
||||
|
||||
this.draw();
|
||||
},
|
||||
|
||||
draw() {
|
||||
this.clear();
|
||||
this.grid(25);
|
||||
var p = this.points[0];
|
||||
this.points.forEach(n => {
|
||||
this.stroke(200);
|
||||
this.line(n.x, n.y, p.x, p.y);
|
||||
p = n;
|
||||
this.stroke(0);
|
||||
this.circle(p.x, p.y, 4);
|
||||
});
|
||||
this.drawSplineData();
|
||||
},
|
||||
|
||||
drawSplineData() {
|
||||
if (this.points.length <= this.degree) return;
|
||||
var mapped = this.points.map(p => [p.x, p.y]);
|
||||
this.drawCurve(mapped);
|
||||
this.drawKnots(mapped);
|
||||
}
|
||||
};
|
43
components/sections/bsplines/uniform-bspline.js
Normal file
43
components/sections/bsplines/uniform-bspline.js
Normal file
@@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
degree: 3,
|
||||
activeDistance: 9,
|
||||
|
||||
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);
|
||||
if(this.props.controller) {
|
||||
this.props.controller(this, this.knots);
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user