mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-09-01 20:33:34 +02:00
regenerated all images
This commit is contained in:
@@ -24,7 +24,7 @@ draw() {
|
||||
end();
|
||||
|
||||
setFill(`black`);
|
||||
text(`Approximating with ${this.steps} strips: ${computedArea} (true area: ${trueArea}`, w/2, h-10, CENTER);
|
||||
text(`Approximating with ${this.steps} strips : ${computedArea} (true area: ${trueArea})`, w/2, h-10, CENTER);
|
||||
}
|
||||
|
||||
drawSlices(w, h, a, n) {
|
||||
|
@@ -7,11 +7,11 @@ If we combine the work done in the previous sections on curve flattening and arc
|
||||
<div class="figure">
|
||||
|
||||
<graphics-element title="Approximate quadratic curve arc length" src="./approximate.js" data-type="quadratic">
|
||||
<input type="range" min="1" max="24" step="1" value="4" class="slide-control">
|
||||
<input type="range" min="2" max="24" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="Approximate cubic curve arc length" src="./approximate.js" data-type="cubic">
|
||||
<input type="range" min="1" max="32" step="1" value="8" class="slide-control">
|
||||
<input type="range" min="2" max="32" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
</div>
|
||||
|
@@ -19,7 +19,7 @@ draw() {
|
||||
setStroke(`lightgrey`);
|
||||
drawGrid(20);
|
||||
|
||||
setStroke(`#CC00CC99`);
|
||||
setStroke(this.parameters.showCurves ? `transparent` : `#CC00CC99`);
|
||||
for (let i=0, e=points.length-1, p, n; i<e; i++) {
|
||||
p = points[i];
|
||||
n = points[i+1];
|
||||
@@ -27,7 +27,10 @@ draw() {
|
||||
}
|
||||
|
||||
setColor(`black`);
|
||||
points.forEach(p => circle(p.x, p.y, 3));
|
||||
points.forEach((p,i) => {
|
||||
circle(p.x, p.y, 3)
|
||||
text(`${i+1}`, p.x+5, p.y+5);
|
||||
});
|
||||
|
||||
this.drawSplineData();
|
||||
}
|
||||
@@ -36,10 +39,17 @@ drawSplineData() {
|
||||
// we'll need at least 4 points
|
||||
if (points.length <= 3) return;
|
||||
|
||||
if (this.parameters.showCurves) {
|
||||
for(let i=0; i<points.length-3; i++) {
|
||||
let c = new Bezier(this, points.slice(i,i+4));
|
||||
c.drawCurve(randomColor());
|
||||
}
|
||||
}
|
||||
|
||||
let spline = new BSpline(this, points);
|
||||
|
||||
noFill();
|
||||
setStroke(`black`);
|
||||
setStroke(this.parameters.showCurves ? `#00000040` : `black`);
|
||||
start();
|
||||
spline.getLUT((points.length - 3) * 20).forEach(p => vertex(p.x, p.y));
|
||||
end();
|
||||
|
@@ -15,6 +15,10 @@ Consider the difference to be this:
|
||||
- for Bézier curves, the curve is defined as an interpolation of points, but:
|
||||
- for B-Splines, the curve is defined as an interpolation of *curves*.
|
||||
|
||||
In fact, let's look at that again, but this time with the base curves shown, too. Each consecutive four points defined one curve:
|
||||
|
||||
<graphics-element title="The components of a B-Spline " width="600" height="300" src="./basic.js" data-show-curves="true"></graphics-element>
|
||||
|
||||
In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work.
|
||||
|
||||
|
||||
|
@@ -31,7 +31,7 @@ setup() {
|
||||
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));
|
||||
addSlider(`slide-control`, `!knot ${i+1}`, min, max, 0.01, i, (v) => this.setKnotValue(i,v));
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,7 @@ setup() {
|
||||
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));
|
||||
addSlider(`slide-control`, `!weight ${i+1}`, 0, 10, 0.1, i%2===1? 2 : 6, v => this.setWeight(i, v));
|
||||
});
|
||||
|
||||
points = points.concat(points.slice(0,3));
|
||||
|
@@ -20,7 +20,7 @@ setup() {
|
||||
|
||||
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));
|
||||
addSlider(`slide-control`, `!knot ${i+1}`, min, max, 0.01, knots[i], v => this.setKnotValue(i, v));
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,7 @@ setup() {
|
||||
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));
|
||||
addSlider(`slide-control`, `!knot ${i+1}`, min, max, 0.01, knots[i], v => this.setKnotValue(i, v));
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -21,6 +21,8 @@ transformTension(v) {
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
setStroke(`lightgrey`);
|
||||
drawGrid(20);
|
||||
|
||||
const p = points, n = points.length;
|
||||
for (let i=1, e=n-2; i<e; i++) {
|
||||
|
@@ -255,7 +255,7 @@ So let's try it out! The following graphic lets you place points, and will start
|
||||
|
||||
<graphics-element title="Fitting a Bézier curve" width="550" src="./curve-fitting.js" >
|
||||
<button class="toggle">toggle</button>
|
||||
<div class="sliders"><!-- this will contain range inputs, created by the graphic --></div>
|
||||
<!-- additional sliders will get created on the fly -->
|
||||
</graphics-element>
|
||||
|
||||
You'll note there is a convenient "toggle" buttons that lets you toggle between equidistant `t` values, and distance ratio along the polygon formed by the points. Arguably more interesting is that once you have points to abstract a curve, you also get <em>direct control</em> over the time values through sliders for each, because if the time values are our degree of freedom, you should be able to freely manipulate them and see what the effect on your curve is.
|
||||
|
@@ -110,7 +110,7 @@ updateSliders() {
|
||||
const l = points.length-1;
|
||||
if (l >= 2) {
|
||||
points.forEach((_,i) => {
|
||||
addSlider(`slide-control`, false, 0, 1, 0.01, i/l, v => this.setTvalue(i, v));
|
||||
addSlider(`slide-control`, `!t<sub>${i}</sub>`, 0, 1, 0.01, i/l, v => this.setTvalue(i, v));
|
||||
});
|
||||
}
|
||||
this.label = `Using equidistant t values`;
|
||||
|
@@ -17,9 +17,9 @@ The following graphic applies this algorithm to a pair of cubic curves, one step
|
||||
(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)
|
||||
|
||||
<graphics-element title="Curve/curve intersections" width="825" src="./curve-curve.js">
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
<button class="next">Advance one step</button>
|
||||
<button class="reset">Reset</button>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at `t=0.5` first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each iteration, and each successive step homing in on the curve's self-intersection points.
|
||||
|
@@ -4,7 +4,7 @@ setup() {
|
||||
setPanelCount(3);
|
||||
this.pairReset();
|
||||
this.setupEventListening();
|
||||
setSlider(`.slide-control`, `epsilon`, 1.0, v => this.reset(v));
|
||||
setSlider(`.slide-control`, `threshold`, 1.0, v => this.reset(v));
|
||||
}
|
||||
|
||||
pairReset() {
|
||||
@@ -52,7 +52,7 @@ draw() {
|
||||
|
||||
this.drawIteration();
|
||||
setFill(`black`);
|
||||
let information = `Initial curves, threshold = ${this.epsilon}px`
|
||||
let information = `Initial curves, threshold = ${this.threshold}px`
|
||||
if (this.step) {
|
||||
information = `Curve collection at iteration ${this.step}`;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ drawIteration() {
|
||||
const pairs = this.pairs;
|
||||
this.pairs = [];
|
||||
pairs.forEach(pair => {
|
||||
if(pair[0].length() < this.epsilon && pair[1].length() < this.epsilon)
|
||||
if(pair[0].length() < this.threshold && pair[1].length() < this.threshold)
|
||||
return this.finals.push(pair);
|
||||
|
||||
// split two curves into four curves
|
||||
|
@@ -73,6 +73,6 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
The following two graphics show the tangent and normal along a quadratic and cubic curve, with the direction vector coloured blue, and the normal vector coloured red (the markers are spaced out evenly as *t*-intervals, not spaced equidistant).
|
||||
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier tangents and normals" src="./quadratic.js"></graphics-element>
|
||||
<graphics-element title="Cubic Bézier tangents and normals" src="./cubic.js"></graphics-element>
|
||||
<graphics-element title="Quadratic Bézier tangents and normals" src="./pointvectors.js" data-type="quadratic"></graphics-element>
|
||||
<graphics-element title="Cubic Bézier tangents and normals" src="./pointvectors.js" data-type="cubic"></graphics-element>
|
||||
</div>
|
||||
|
@@ -1,7 +1,16 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
curve = Bezier.defaultCubic(this);
|
||||
const type = this.type = this.parameters.type ?? `quadratic`;
|
||||
if (type === `quadratic`) {
|
||||
curve = Bezier.defaultQuadratic(this);
|
||||
} else {
|
||||
curve = Bezier.defaultCubic(this);
|
||||
curve.points[0].x = 30;
|
||||
curve.points[0].y = 230;
|
||||
curve.points[1].x = 75;
|
||||
curve.points[1].y = 50;
|
||||
}
|
||||
setMovable(curve.points);
|
||||
}
|
||||
|
||||
@@ -15,7 +24,7 @@ draw() {
|
||||
for(let i=0; i<=10; i++) {
|
||||
let t = i/10.0;
|
||||
let p = curve.get(t);
|
||||
let d = this.getDerivative(t, pts);
|
||||
let d = this.type === `quadratic` ? this.getQuadraticDerivative(t, pts) : this.getCubicDerivative(t, pts);
|
||||
|
||||
let m = sqrt(d.x*d.x + d.y*d.y);
|
||||
d = { x: d.x/m, y: d.y/m };
|
||||
@@ -27,9 +36,6 @@ draw() {
|
||||
setStroke(`red`);
|
||||
line(p.x, p.y, p.x + n.x*f, p.y + n.y*f);
|
||||
|
||||
setStroke(`purple`);
|
||||
line(p.x, p.y, p.x - n.x*f, p.y - n.y*f);
|
||||
|
||||
setStroke(`black`);
|
||||
circle(p.x, p.y, 3);
|
||||
}
|
||||
@@ -37,7 +43,25 @@ draw() {
|
||||
curve.drawPoints();
|
||||
}
|
||||
|
||||
getDerivative(t, points) {
|
||||
getQuadraticDerivative(t, points) {
|
||||
let mt = (1 - t), d = [
|
||||
{
|
||||
x: 2 * (points[1].x - points[0].x),
|
||||
y: 2 * (points[1].y - points[0].y)
|
||||
},
|
||||
{
|
||||
x: 2 * (points[2].x - points[1].x),
|
||||
y: 2 * (points[2].y - points[1].y)
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
x: mt * d[0].x + t * d[1].x,
|
||||
y: mt * d[0].y + t * d[1].y
|
||||
};
|
||||
}
|
||||
|
||||
getCubicDerivative(t, points) {
|
||||
let mt = (1 - t), a = mt*mt, b = mt*t, c = t*t, d = [
|
||||
{
|
||||
x: 3 * (points[1].x - points[0].x),
|
@@ -1,61 +0,0 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
curve = Bezier.defaultQuadratic(this);
|
||||
setMovable(curve.points);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
curve.drawSkeleton();
|
||||
|
||||
const pts = curve.points;
|
||||
const f = 15;
|
||||
|
||||
for(let i=0; i<=10; i++) {
|
||||
let t = i/10.0;
|
||||
let p = curve.get(t);
|
||||
let d = this.getDerivative(t, pts);
|
||||
|
||||
let m = sqrt(d.x*d.x + d.y*d.y);
|
||||
d = { x: d.x/m, y: d.y/m };
|
||||
let n = this.getNormal(t, d);
|
||||
|
||||
setStroke(`blue`);
|
||||
line(p.x, p.y, p.x + d.x*f, p.y + d.y*f);
|
||||
|
||||
setStroke(`red`);
|
||||
line(p.x, p.y, p.x + n.x*f, p.y + n.y*f);
|
||||
|
||||
setStroke(`purple`);
|
||||
line(p.x, p.y, p.x - n.x*f, p.y - n.y*f);
|
||||
|
||||
setStroke(`black`);
|
||||
circle(p.x, p.y, 3);
|
||||
}
|
||||
|
||||
curve.drawPoints();
|
||||
}
|
||||
|
||||
getDerivative(t, points) {
|
||||
let mt = (1 - t), d = [
|
||||
{
|
||||
x: 2 * (points[1].x - points[0].x),
|
||||
y: 2 * (points[1].y - points[0].y)
|
||||
},
|
||||
{
|
||||
x: 2 * (points[2].x - points[1].x),
|
||||
y: 2 * (points[2].y - points[1].y)
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
x: mt * d[0].x + t * d[1].x,
|
||||
y: mt * d[0].y + t * d[1].y
|
||||
};
|
||||
}
|
||||
|
||||
getNormal(t, d) {
|
||||
const q = sqrt(d.x * d.x + d.y * d.y);
|
||||
return { x: -d.y / q, y: d.x / q };
|
||||
}
|
@@ -28,7 +28,7 @@ And then we're done, we found "the" normal vector for a 3D curve. Let's see what
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
However, if you've played with that graphic a bit, you might have noticed something odd. The normal seems to "suddenly twist around" around between t=0.5 and t=0.9... Why is doing that?
|
||||
However, if you've played with that graphic a bit, you might have noticed something odd. The normal seems to "suddenly twist around the curve" between t=0.65 and t=0.75... Why is it doing that?
|
||||
|
||||
As it turns out, it's doing that because that's how the maths works, and that's the problem with Frenet normals: while they are "mathematically correct", they are "practically problematic", and so for any kind of graphics work what we really want is a way to compute normals that just... look good.
|
||||
|
||||
|
@@ -76,14 +76,14 @@ movePointsQuadratic(i, link) {
|
||||
let pl = points[(l+i-1)%l];
|
||||
let pr = points[(l+i+1)%l];
|
||||
let ppr = points[(l+i+3)%l];
|
||||
pl.x += this.currentPoint.diff.x;
|
||||
pl.y += this.currentPoint.diff.y;
|
||||
pr.x += this.currentPoint.diff.x;
|
||||
pr.y += this.currentPoint.diff.y;
|
||||
ppl.x -= this.currentPoint.diff.x;
|
||||
ppl.y -= this.currentPoint.diff.y;
|
||||
ppr.x -= this.currentPoint.diff.x;
|
||||
ppr.y -= this.currentPoint.diff.y;
|
||||
pl.x += this.cursor.diff.x;
|
||||
pl.y += this.cursor.diff.y;
|
||||
pr.x += this.cursor.diff.x;
|
||||
pr.y += this.cursor.diff.y;
|
||||
ppl.x -= this.cursor.diff.x;
|
||||
ppl.y -= this.cursor.diff.y;
|
||||
ppr.x -= this.cursor.diff.x;
|
||||
ppr.y -= this.cursor.diff.y;
|
||||
|
||||
this.problem = points[(l+i+4)%l];
|
||||
if (ppr.y === this.problem.y) {
|
||||
@@ -129,11 +129,12 @@ movePointsCubic(i, link) {
|
||||
const l = points.length;
|
||||
if (link === `conventional` && i%3 === 0) {
|
||||
let left = points[(l+i-1)%l];
|
||||
left.x += this.currentPoint.diff.x;
|
||||
left.y += this.currentPoint.diff.y;
|
||||
left.x += this.cursor.diff.x;
|
||||
left.y += this.cursor.diff.y;
|
||||
|
||||
let right = points[(l+i+1)%l];
|
||||
right.x += this.currentPoint.diff.x;
|
||||
right.y += this.currentPoint.diff.y;
|
||||
right.x += this.cursor.diff.x;
|
||||
right.y += this.cursor.diff.y;
|
||||
}
|
||||
|
||||
if (i%3 > 0) {
|
||||
|
@@ -24,4 +24,4 @@ This makes the interval we check smaller and smaller at each iteration, and we c
|
||||
|
||||
So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bézier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.
|
||||
|
||||
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./project.js"></graphics-element>
|
||||
<graphics-element title="Projecting a point onto a Bézier curve" width="400" height="400" src="./project.js"></graphics-element>
|
||||
|
@@ -2,16 +2,16 @@ let curve;
|
||||
|
||||
setup() {
|
||||
curve = new Bezier(this, [
|
||||
{x:248,y:188},
|
||||
{x:218,y:294},
|
||||
{x:45,y:290},
|
||||
{x:12,y:236},
|
||||
{x:14,y:82},
|
||||
{x:186,y:177},
|
||||
{x:221,y:90},
|
||||
{x:18,y:156},
|
||||
{x:34,y:57},
|
||||
{x:198,y:18}
|
||||
{x:288,y:218},
|
||||
{x:258,y:334},
|
||||
{x:85,y:330},
|
||||
{x:52,y:276},
|
||||
{x:54,y:122},
|
||||
{x:216,y:217},
|
||||
{x:261,y:130},
|
||||
{x:58,y:196},
|
||||
{x:84,y:97},
|
||||
{x:238,y:58}
|
||||
]);
|
||||
|
||||
this.cursor.x = 280;
|
||||
|
@@ -23,6 +23,8 @@ bindButtons() {
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
setStroke(`lightgrey`);
|
||||
drawGrid(20);
|
||||
this.drawCurve();
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ drawCurve() {
|
||||
// And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm:
|
||||
start();
|
||||
noFill();
|
||||
setStroke(`black`);
|
||||
for(let t=0; t<=1; t+=0.01) {
|
||||
let q = JSON.parse(JSON.stringify(points));
|
||||
while(q.length > 1) {
|
||||
@@ -113,9 +116,9 @@ lower() {
|
||||
// And then we map our k-order list of coordinates
|
||||
// to an n-order list of coordinates, instead:
|
||||
const V = Mi.multiply(Mt);
|
||||
const x = new Matrix(pts.map(p => [p.x]));
|
||||
const x = new Matrix(points.map(p => [p.x]));
|
||||
const nx = V.multiply(x);
|
||||
const y = new Matrix(pts.map(p => [p.y]));
|
||||
const y = new Matrix(points.map(p => [p.y]));
|
||||
const ny = V.multiply(y);
|
||||
|
||||
points = nx.data.map((x,i) => ({
|
||||
|
@@ -23,10 +23,10 @@ This does something unexpected: it turns our polynomial into something that _isn
|
||||
But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:
|
||||
|
||||
<graphics-element title="Our rational cubic Bézier curve" src="./rational.js">
|
||||
ratio 1 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
ratio 2 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
ratio 3 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
ratio 4 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span>
|
||||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-1">
|
||||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-2">
|
||||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-3">
|
||||
<input type="range" min="0.01" max="2" value="1" step="0.01" class="ratio-4">
|
||||
</graphics-element>
|
||||
|
||||
You can think of the ratio values as each coordinate's "gravity": the higher the gravity, the closer to that coordinate the curve will want to be. You'll also notice that if you simply increase or decrease all the ratios by the same amount, nothing changes... much like with gravity, if the relative strengths stay the same, nothing really changes. The values define each coordinate's influence _relative to all other points_.
|
||||
|
@@ -1,24 +1,17 @@
|
||||
let curve;
|
||||
let curve, ratios=[1, 1, 1, 1];
|
||||
|
||||
setup() {
|
||||
curve = Bezier.defaultCubic(this);
|
||||
setMovable(curve.points);
|
||||
curve.points.forEach((p,i) => {
|
||||
setSlider(`.ratio-${i+1}`, `!ratio-${i+1}`, 1, v => this.setRatio(i,v))
|
||||
});
|
||||
}
|
||||
|
||||
const inputs = findAll(`input[type=range]`);
|
||||
if (inputs) {
|
||||
const ratios = inputs.map(i => parseFloat(i.value));
|
||||
curve.setRatios(ratios);
|
||||
|
||||
inputs.forEach((input,pos) => {
|
||||
const span = input.nextSibling;
|
||||
input.listen(`input`, evt => {
|
||||
const value = parseFloat(evt.target.value);
|
||||
span.textContent = ratios[pos] = value;
|
||||
curve.update();
|
||||
this.redraw();
|
||||
});
|
||||
})
|
||||
}
|
||||
setRatio(i, v) {
|
||||
ratios[i] = v;
|
||||
curve.setRatios(ratios);
|
||||
redraw();
|
||||
}
|
||||
|
||||
draw() {
|
||||
|
Reference in New Issue
Block a user