mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-31 12:01:54 +02:00
tracing
This commit is contained in:
@@ -8,9 +8,9 @@ What we want is to ensure that the [curvature](https://en.wikipedia.org/wiki/Cur
|
||||
|
||||
Problem solved!
|
||||
|
||||
However, there's a problem with this approach: if we think about this a little more, we realise that "what a curve looks like" and its derivative values are pretty much entirely unrelated. After all, the section on [reordering curves](#reordering) showed us that the same looking curve can have an infinite number of curve expressions of arbitraryly high Bezier degree, and each of those will have _widly_ different derivative values.
|
||||
However, there's a problem with this approach: if we think about this a little more, we realise that "what a curve looks like" and its derivative values are pretty much entirely unrelated. After all, the section on [reordering curves](#reordering) showed us that the same looking curve can have an infinite number of curve expressions of arbitraryly high Bézier degree, and each of those will have _widly_ different derivative values.
|
||||
|
||||
So what we really want is some kind of expression that's not based on any particular expression of `t`, but is based on something that is invariant to the _kind_ of function(s) we use to draw our curve. And the prime candidate for this is our curve expression, reparameterised for distance: no matter what order of Bezier curve we use, if we were able to rewrite it as a function of distance-along-the-curve, all those different degree Bezier functions would end up being _the same_ function for "coordinate at some distance D along the curve".
|
||||
So what we really want is some kind of expression that's not based on any particular expression of `t`, but is based on something that is invariant to the _kind_ of function(s) we use to draw our curve. And the prime candidate for this is our curve expression, reparameterised for distance: no matter what order of Bézier curve we use, if we were able to rewrite it as a function of distance-along-the-curve, all those different degree Bézier functions would end up being _the same_ function for "coordinate at some distance D along the curve".
|
||||
|
||||
We've seen this before... that's the arc length function.
|
||||
|
||||
|
@@ -3,8 +3,9 @@ let q, c;
|
||||
setup() {
|
||||
q = new Bezier(this, 60,55, 125,160, 365,165);
|
||||
c = new Bezier(this, 385,165, 645,165, 645,70, 750,165);
|
||||
|
||||
setSlider(`.slide-control`, `position`, 0);
|
||||
if (this.parameters.omni) {
|
||||
setSlider(`.slide-control`, `position`, 0);
|
||||
}
|
||||
setMovable(q.points.concat(c.points));
|
||||
}
|
||||
|
||||
|
@@ -251,7 +251,7 @@ Here, the "to the power negative one" is the notation for the [matrix inverse](h
|
||||
|
||||
So before we try that out, how much code is involved in implementing this? Honestly, that answer depends on how much you're going to be writing yourself. If you already have a matrix maths library available, then really not that much code at all. On the other hand, if you are writing this from scratch, you're going to have to write some utility functions for doing your matrix work for you, so it's really anywhere from 50 lines of code to maybe 200 lines of code. Not a bad price to pay for being able to fit curves to prespecified coordinates.
|
||||
|
||||
So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least three. You can click for more points, and the code will simply try to compute an exact fit using a Bezier curve of the appropriate order. Four points? Cubic Bezier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once you hit 10<sup>th</sup> or higher order curves. But it might not!
|
||||
So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least three. You can click for more points, and the code will simply try to compute an exact fit using a Bézier curve of the appropriate order. Four points? Cubic Bézier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once you hit 10<sup>th</sup> or higher order curves. But it might not!
|
||||
|
||||
<div class="figure">
|
||||
<Graphic title="Fitting a Bézier curve" setup={this.setup} draw={this.draw} onClick={this.onClick}>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# Drawing Bezier paths
|
||||
# Drawing Bézier paths
|
||||
|
||||
- draw with a mouse, stylus, or finger
|
||||
- RDP to reduce the number of points along the path
|
||||
- abstract curve through points:
|
||||
- high order bezier, split and reduced
|
||||
- fit compound bezier
|
||||
- high order Bézier, split and reduced
|
||||
- fit compound Bézier
|
||||
- catmull-rom
|
||||
|
||||
<div class="figure">
|
||||
|
@@ -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).
|
||||
|
46
docs/chapters/tracing/distance-function.js
Normal file
46
docs/chapters/tracing/distance-function.js
Normal 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();
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
103
docs/chapters/tracing/tracing.js
Normal file
103
docs/chapters/tracing/tracing.js
Normal 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);
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
# Finding Y, given X
|
||||
|
||||
One common task that pops up in things like CSS work, or parametric equalisers, or image leveling, or any other number of applications where Bezier curves are used as control curves in a way that there is really only ever one "y" value associated with one "x" value, you might want to cut out the middle man, as it were, and compute "y" directly based on "x". After all, the function looks simple enough, finding the "y" value should be simple too, right? Unfortunately, not really. However, it _is_ possible and as long as you have some code in place to help, it's not a lot of a work either.
|
||||
One common task that pops up in things like CSS work, or parametric equalisers, or image leveling, or any other number of applications where Bézier curves are used as control curves in a way that there is really only ever one "y" value associated with one "x" value, you might want to cut out the middle man, as it were, and compute "y" directly based on "x". After all, the function looks simple enough, finding the "y" value should be simple too, right? Unfortunately, not really. However, it _is_ possible and as long as you have some code in place to help, it's not a lot of a work either.
|
||||
|
||||
We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bezier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the `x` coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.
|
||||
We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the `x` coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.
|
||||
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" src="./basics.js">
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -33,7 +33,7 @@ You might be wondering "where did all the other 'minus x' for all the other valu
|
||||
```
|
||||
// prepare our values for root finding:
|
||||
x = a value we already know
|
||||
xcoord = our set of bezier curve's x coordinates
|
||||
xcoord = our set of Bézier curve's x coordinates
|
||||
foreach p in xcoord: p.x -= x
|
||||
|
||||
// find our root, of which we know there is exactly one:
|
||||
|
Reference in New Issue
Block a user