1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-28 18:49:57 +02:00
This commit is contained in:
Pomax
2020-08-27 21:46:01 -07:00
parent 17d71c7d70
commit 4e34774afb
23 changed files with 278 additions and 215 deletions

View File

@@ -4,20 +4,20 @@ Say you want to draw a curve with a dashed line, rather than a solid line, or yo
Now you have a problem.
The reason you have a problem is that Bézier curves are parametric functions with non-linear behaviour, whereas moving a train along a track is about as close to a practical example of linear behaviour as you can get. The problem we're faced with is that we can't just pick *t* values at some fixed interval and expect the Bézier functions to generate points that are spaced a fixed distance apart. In fact, let's look at the relation between "distance long a curve" and "*t* value", by plotting them against one another.
The reason you have a problem is that Bézier curves are parametric functions with non-linear behaviour, whereas moving a train along a track is about as close to a practical example of linear behaviour as you can get. The problem we're faced with is that we can't just pick `t` values at some fixed interval and expect the Bézier functions to generate points that are spaced a fixed distance apart. In fact, let's look at the relation between "distance long a curve" and "`t` value", by plotting them against one another.
The following graphic shows a particularly illustrative curve, and it's length-to-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). This is, it's safe to say, not what we'll see, we'll see something wobbly instead. To make matters even worse, the length-to-*t* function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form once at best, the plot shows that the distance function along the curve is able to switch forms three times (to see this, try creating an S curve with the start/end close together, but the control points far apart).
The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.
<Graphic title="The t-for-distance function" setup={this.setup} draw={this.plotOnly}/>
<graphics-element title="The t-for-distance function" width="550" src="./distance-function.js"></graphics-element>
We see a function that might be invertible, but we won't be able to do so, symbolically. You may remember from the section on arc length that we cannot actually compute the true arc length function as an expression of *t*, which means we also can't compute the true inverted function that gives *t* as an expression of length. So how do we fix this?
So, how do we "cut up" the arc length function at regular intervals, when we can't really work with it? We basically cheat: we run through the curve using `t` values, determine the distance-for-this-`t`-value at each point we generate during the run, and then we find "the closest `t` value that matches some required distance" using those values instead. If we have a low number of points sampled, we can then even refine which `t` value "should" work for our desired distance by interpolating between two points, but if we have a high enough number of samples, we don't even need to bother.
One way is to do what the graphic does: simply run through the curve, determine its *t*-for-length values as a set of discrete values at some high resolution (the graphic uses 100 discrete points), and then use those as a basis for finding an appropriate *t* value, given a distance along the curve. This works quite well, actually, and is fairly fast.
So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those `t` values on the real curve looks like, by coloring each section of curve between two distance markers differently:
We can use some colour to show the difference between distance-based and time based intervals: the following graph is similar to the previous one, except it segments the curve in terms of equal-distance intervals. This shows as regular colour intervals going down the graph, but the mapping to *t* values is not linear, so there will be (highly) irregular intervals along the horizontal axis. It also shows the curve in an alternating colouring based on the t-for-distance values we find our LUT:
<graphics-element title="Fixed-interval coloring a curve" width="825" src="./tracing.js">
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
</graphics-element>
<Graphic title="Fixed-interval coloring a curve" setup={this.setup} draw={this.drawColoured} onKeyDown={this.props.onKeyDown}/>
Use your up and down arrow keys to increase or decrease the number of equidistant segments used to colour the curve.
Use the slider to increase or decrease the number of equidistant segments used to colour the curve.
However, are there better ways? One such way is discussed in "[Moving Along a Curve with Specified Speed](http://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf)" by David Eberly of Geometric Tools, LLC, but basically because we have no explicit length function (or rather, one we don't have to constantly compute for different intervals), you may simply be better off with a traditional lookup table (LUT).

View File

@@ -0,0 +1,46 @@
let curve;
setup(api) {
curve = new Bezier(this, 65, 150, 15, 35, 175, 245, 35, 140);
setMovable(curve.points);
}
draw() {
resetTransform();
clear();
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
let w = this.width/2;
let h = this.height;
let len = curve.length();
translate(w,0);
line(0, 0, 0, h);
scale(0.85);
translate(30,30);
setStroke(`black`);
drawAxes("t", 0, 1, "d", 0, len|0, w, h);
this.plotDistanceFunction(w, h, len);
}
plotDistanceFunction(w, h, len) {
noFill();
let LUT = curve.getLUT(this.steps * 10);
let d = 0;
start();
vertex(0,0);
for(let i=1, e=LUT.length, p1, p2; i<e; i++) {
p1 = LUT[i-1];
p2 = LUT[i];
d += dist(p1.x, p1.y, p2.x, p2.y);
vertex(
map(i, 0, e, 0, w),
map(d, 0, len, 0, h)
);
}
end();
}

View File

@@ -1,126 +0,0 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
setup: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 8;
},
generate: function(api, curve, offset, pad, fwh) {
offset.x += pad;
offset.y += pad;
var len = curve.length();
var pts = [{x:0, y:0, d:0}];
for(var v=1, t, d; v<=100; v++) {
t = v/100;
d = curve.split(t).left.length();
pts.push({
x: api.utils.map(t, 0,1, 0,fwh),
y: api.utils.map(d, 0,len, 0,fwh),
d: d,
t: t
});
}
return pts;
},
draw: function(api, curve, offset) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var len = curve.length();
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var pad = 20;
var fwh = w - 2*pad;
offset.x += w;
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "d",0,len, offset);
return this.generate(api, curve, offset, pad, fwh);
},
plotOnly: function(api, curve) {
api.setPanelCount(2);
var offset = {x:0, y:0};
var pts = this.draw(api, curve, offset);
for(var i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
},
drawColoured: function(api, curve) {
api.setPanelCount(3);
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var pad = 20;
var fwh = w - 2*pad;
var offset = {x:0, y:0};
var len = curve.length();
var pts = this.draw(api, curve, offset);
var s = api.steps, i, p, ts=[];
for(i=0; i<=s; i++) {
var target = (i * len)/s;
// find the t nearest our target distance
for (p=0; p<pts.length; p++) {
if (pts[p].d > target) {
p--;
break;
}
}
if(p<0) p=0;
if(p===pts.length) p=pts.length-1;
ts.push(pts[p]);
}
for(i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
ts.forEach(p => {
var pt = { x: api.utils.map(p.t,0,1,0,fwh), y: 0 };
var pd = { x: 0, y: api.utils.map(p.d,0,len,0,fwh) };
api.setColor("black");
api.drawCircle(pt, 3, offset);
api.drawCircle(pd, 3, offset);
api.setColor("lightgrey");
api.drawLine(pt, {x:pt.x, y:pd.y}, offset);
api.drawLine(pd, {x:pt.x, y:pd.y}, offset);
});
offset = {x:2*w, y:0};
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
var idx=0, colors = ["rgb(240,0,200)", "rgb(0,40,200)"];
api.setColor(colors[idx]);
var p0 = curve.get(pts[0].t), p1;
api.drawCircle(curve.get(0), 4, offset);
for (i=1, p1; i<pts.length; i++) {
p1 = curve.get(pts[i].t);
api.drawLine(p0, p1, offset);
if (ts.indexOf(pts[i]) !== -1) {
api.setColor(colors[++idx % colors.length]);
api.drawCircle(p1, 4, offset);
}
p0 = p1;
}
}
};

View File

@@ -0,0 +1,103 @@
let curve;
setup(api) {
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `steps`, 8);
}
draw() {
resetTransform();
clear();
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
let w = this.width/3;
let h = this.height;
let len = curve.length();
setStroke(`black`);
translate(w,0);
line(0, 0, 0, h);
scale(0.85);
translate(30,30);
setFill(`black`);
drawAxes("t", 0, 1, "d", 0, len|0, w, h);
let LUT = this.plotDistanceFunction(w, h, len);
this.drawPlotIntervals(w, h, LUT);
resetTransform();
translate(2*w,0);
line(0, 0, 0, h);
this.drawCurveIntervals(LUT);
}
plotDistanceFunction(w, h, len) {
noFill();
let LUT = curve.getLUT(this.steps * 10);
let d = LUT[0].d = 0;
LUT[0].t = 0;
start();
vertex(0,0);
for(let i=1, e=LUT.length-1, p1, p2; i<=e; i++) {
p1 = LUT[i-1];
p2 = LUT[i];
d += dist(p1.x, p1.y, p2.x, p2.y);
vertex(
map(i, 0, e, 0, w),
map(d, 0, len, 0, h)
);
p2.d = d;
p2.t = i/e;
}
end();
return LUT;
}
drawPlotIntervals(w, h, LUT) {
noFill();
setStroke(`grey`);
let dlen = LUT.slice(-1)[0].d;
let pos = 0;
for(let i=0, e=this.steps; i<=e; i++) {
// get our closest known coordinate
let targetDistance = i/e * dlen;
while(LUT[pos].d < targetDistance) pos++;
// then we can either refine this to get a more exact
// associated `t`, but really, there's no reason to if
// we care about integer coordinates...
let l = LUT[pos];
let x = map(pos, 0, LUT.length-1, 0, w);
let y = map(l.d, 0, dlen, 0, h);
line(0,y,x,y);
line(x,0,x,y);
}
}
drawCurveIntervals(LUT) {
noFill();
setStroke(`red`);
let dlen = LUT.slice(-1)[0].d;
let lastpos = 0, pos = 0;
for(let i=0, e=this.steps; i<=e; i++) {
let targetDistance = i/e * dlen;
while(LUT[pos].d < targetDistance) pos++;
setStroke( randomColor() );
start();
for(let j=lastpos; j<=pos; j++) vertex(LUT[j].x, LUT[j].y);
lastpos = pos;
end();
let p = curve.get(LUT[pos].t);
setStroke(`black`);
circle(p.x, p.y, 1);
}
}