tracing
@@ -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);
|
||||
|
||||
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
@@ -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
@@ -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:
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 16 KiB |
@@ -353,7 +353,7 @@ function Bezier(3,t):
|
||||
|
||||
<p>Also shown is the interpolation function for a 15<sup>th</sup> order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.</p>
|
||||
<p>If we want to change the curve, we need to change the weights of each point, effectively changing the interpolations. The way to do this is about as straightforward as possible: just multiply each point with a value that changes its strength. These values are conventionally called "weights", and we can add them to our original Bézier function:</p>
|
||||
<script>console.log("LaTeX for 14cb9fbbaae9e7d87ae6bef3ea7a782e failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/control/14cb9fbbaae9e7d87ae6bef3ea7a782e.svg" width="379px" height="56px" loading="lazy">
|
||||
<p>That looks complicated, but as it so happens, the "weights" are actually just the coordinate values we want our curve to have: for an <i>n<sup>th</sup></i> order curve, w<sub>0</sub> is our start coordinate, w<sub>n</sub> is our last coordinate, and everything in between is a controlling coordinate. Say we want a cubic curve that starts at (110,150), is controlled by (25,190) and (210,250) and ends at (210,30), we use this Bézier curve:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/control/c0d4dbc07b8ec7c0a18ea43c8a386935.svg" width="476px" height="40px" loading="lazy">
|
||||
<p>Which gives us the curve we saw at the top of the article:</p>
|
||||
@@ -637,7 +637,7 @@ function drawCurve(points[], t):
|
||||
<section id="matrixsplit">
|
||||
<h1><a href="#matrixsplit">Splitting curves using matrices</a></h1>
|
||||
<p>Another way to split curves is to exploit the matrix representation of a Bézier curve. In <a href="#matrix">the section on matrices</a>, we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves respectively: (we'll reverse the Bézier coefficients vector for legibility)</p>
|
||||
<script>console.log("LaTeX for 77a11d65d7cffc4b84a85c4bec837792 failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/matrixsplit/77a11d65d7cffc4b84a85c4bec837792.svg" width="263px" height="55px" loading="lazy">
|
||||
<p>and</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/matrixsplit/c58330e12d25c678b593ddbd4afa7c52.svg" width="323px" height="73px" loading="lazy">
|
||||
<p>Let's say we want to split the curve at some point <code>t = z</code>, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we separate out the actual "point on the curve" information into a new matrix multiplication:</p>
|
||||
@@ -1271,8 +1271,8 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
</section>
|
||||
<section id="yforx">
|
||||
<h1><a href="#yforx">Finding Y, given X</a></h1>
|
||||
<p>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 <em>is</em> possible and as long as you have some code in place to help, it's not a lot of a work either.</p>
|
||||
<p>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 <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<p>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 <em>is</em> possible and as long as you have some code in place to help, it's not a lot of a work either.</p>
|
||||
<p>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 <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\a6f705eb306c43e5709970b2ccad9d20.png" loading="lazy">
|
||||
@@ -1291,7 +1291,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they all cancel out, so the only one we actually need to subtract is the one at the end. Handy! So now we just solve this equation using Cardano's algorithm, and we're left with some rather short code:</p>
|
||||
<pre><code>// 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:
|
||||
@@ -1395,8 +1395,8 @@ y = curve.get(t).y</code></pre>
|
||||
<p>For instance, we can start by ensuring that the two curves share an end coordinate, so that there is no "gap" between the end of one and the start of the next curve, but that won't guarantee that things look right: both curves can be going in wildly different directions, and the resulting joined geometry will have a corner in it, rather than a smooth transition from one curve to the next.</p>
|
||||
<p>What we want is to ensure that the <a href="https://en.wikipedia.org/wiki/Curvature">curvature</a> at the transition from one curve to the next "looks good". So, we start with a shared coordinate, and then also require that derivatives for both curves match at that coordinate. That way, we're assured that their tangents line up, which must mean the curve transition is perfectly smooth. We can even make the second, third, etc. derivatives match up for better and better transitions.</p>
|
||||
<p>Problem solved!</p>
|
||||
<p>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 <a href="#reordering">reordering curves</a> 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 <em>widly</em> different derivative values.</p>
|
||||
<p>So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on something that is invariant to the <em>kind</em> 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 <em>the same</em> function for "coordinate at some distance D along the curve".</p>
|
||||
<p>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 <a href="#reordering">reordering curves</a> 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 <em>widly</em> different derivative values.</p>
|
||||
<p>So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on something that is invariant to the <em>kind</em> 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 <em>the same</em> function for "coordinate at some distance D along the curve".</p>
|
||||
<p>We've seen this before... that's the arc length function.</p>
|
||||
<p>So you might think that in order to find the curvature of a curve, we now need to solve the arc length function itself, and that this would be quite a problem because we just saw that there is no way to actually do that. Thankfully, we don't. We only need to know the <em>form</em> of the arc length function, which we saw above and is fairly simple, rather than needing to <em>solve</em> the arc length function. If we start with the arc length expression and the <a href="http://mathworld.wolfram.com/Curvature.html">run through the steps necessary</a> to determine <em>its</em> derivative (with an alternative, shorter demonstration of how to do this found <a href="https://math.stackexchange.com/a/275324/71940">over on Stackexchange</a>), then the integral that was giving us so much problems in solving the arc length function disappears entirely (because of the <a href="https://en.wikipedia.org/wiki/Fundamental_theorem_of_calculus">fundamental theorem of calculus</a>), and what we're left with us some surprisingly simple maths that relates curvature (denoted as κ, "kappa") to—and this is the truly surprising bit—a specific combination of derivatives of our original function.</p>
|
||||
<p>Let me highlight what just happened, because it's pretty special:</p>
|
||||
@@ -1404,7 +1404,7 @@ y = curve.get(t).y</code></pre>
|
||||
<li>we wanted to make curves line up, and initially thought to match the curves' derivatives, but</li>
|
||||
<li>that turned out to be a really bad choice, so instead</li>
|
||||
<li>we picked a function that is basically impossible to work with, and then <em>worked with that</em>, which</li>
|
||||
<li>gives us a simple formula that is based on <em>the curves' derivatives</em>.</li>
|
||||
<li>gives us a simple formula that is <em>and expression using the curves' derivatives</em>.</li>
|
||||
</ol>
|
||||
<p><em>That's crazy!</em></p>
|
||||
<p>But that's also one of the things that makes maths so powerful: even if your initial ideas are off the mark, you might be much closer than you thought you were, and the journey from "thinking we're completely wrong" to "actually being remarkably close to being right" is where we can find a lot of insight.</p>
|
||||
@@ -1425,7 +1425,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.</p>
|
||||
<graphics-element title="Matching curvatures for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\a98d37a0653461ad4e6065d8277c8834.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\5fcfb0572cae06717506c84768aa568c.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1434,7 +1434,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:</p>
|
||||
<graphics-element title="(Easier) curvature matching for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" data-omni="true">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\1b2e086966d7e8088e4b51a11d9ec063.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\876d7b2750d7c29068ac6181c3634d25.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control">
|
||||
@@ -1445,16 +1445,25 @@ y = curve.get(t).y</code></pre>
|
||||
<h1><a href="#tracing">Tracing a curve at fixed distance intervals</a></h1>
|
||||
<p>Say you want to draw a curve with a dashed line, rather than a solid line, or you want to move something along the curve at fixed distance intervals over time, like a train along a track, and you want to use Bézier curves.</p>
|
||||
<p>Now you have a problem.</p>
|
||||
<p>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 <em>t</em> 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 "<em>t</em> value", by plotting them against one another.</p>
|
||||
<p>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-<em>t</em> 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).</p>
|
||||
<Graphic title="The t-for-distance function" setup={this.setup} draw={this.plotOnly}/>
|
||||
<p>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 <code>t</code> 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 "<code>t</code> value", by plotting them against one another.</p>
|
||||
<p>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.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\4f2cd306ec6fa0340ac7f410744b3118.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>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 <em>t</em>, which means we also can't compute the true inverted function that gives <em>t</em> as an expression of length. So how do we fix this?</p>
|
||||
<p>One way is to do what the graphic does: simply run through the curve, determine its <em>t</em>-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 <em>t</em> value, given a distance along the curve. This works quite well, actually, and is fairly fast.</p>
|
||||
<p>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 <em>t</em> 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:</p>
|
||||
<Graphic title="Fixed-interval coloring a curve" setup={this.setup} draw={this.drawColoured} onKeyDown={this.props.onKeyDown}/>
|
||||
<p>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 <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and then we find "the closest <code>t</code> 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 <code>t</code> 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.</p>
|
||||
<p>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 <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\25e9697557129c651e9c7cc4e4878b16.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<p>Use your up and down arrow keys to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||||
<p>Use the slider to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||||
<p>However, are there better ways? One such way is discussed in "<a href="http://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf">Moving Along a Curve with Specified Speed</a>" 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).</p>
|
||||
|
||||
</section>
|
||||
@@ -1673,7 +1682,7 @@ with quadratic or cubic curves:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/curvefitting/d84d1c71a3ce1918f53eaf8f9fe98ac4.svg" width="168px" height="27px" loading="lazy">
|
||||
<p>Here, the "to the power negative one" is the notation for the <a href="https://en.wikipedia.org/wiki/Invertible_matrix">matrix inverse</a>. But that's all we have to do: we're done. Starting with <strong>P</strong> and inventing some <code>t</code> values based on the polygon the coordinates in <strong>P</strong> define, we can compute the corresponding Bézier coordinates <strong>C</strong> that specify a curve that goes through our points. Or, if it can't go through them exactly, as near as possible.</p>
|
||||
<p>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.</p>
|
||||
<p>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!</p>
|
||||
<p>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!</p>
|
||||
<div class="figure">
|
||||
<Graphic title="Fitting a Bézier curve" setup={this.setup} draw={this.draw} onClick={this.onClick}>
|
||||
<button onClick={this.toggle} style="position:absolute; right: 0;">toggle</button>
|
||||
@@ -1945,7 +1954,7 @@ with quadratic or cubic curves:</p>
|
||||
<p>which we can then substitute in the expression for <em>a</em>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/ef3ab62bb896019c6157c85aae5d1ed3.svg" width="231px" height="195px" loading="lazy">
|
||||
<p>A quick check shows that plugging these values for <em>a</em> and <em>b</em> into the expressions for C<sub>x</sub> and C<sub>y</sub> give the same x/y coordinates for both "<em>a</em> away from A" and "<em>b</em> away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for <em>t=0.5</em> (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:</p>
|
||||
<script>console.log("LaTeX for fe32474b4616ee9478e1308308f1b6bf failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/fe32474b4616ee9478e1308308f1b6bf.svg" width="188px" height="32px" loading="lazy">
|
||||
<p>We compute T, observing that if <em>t=0.5</em>, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/e1059e611aa1e51db41f9ce0b4ebb95a.svg" width="252px" height="35px" loading="lazy">
|
||||
<p>Which, worked out for the x and y components, gives:</p>
|
||||
|
@@ -634,7 +634,7 @@ function drawCurve(points[], t):
|
||||
<section id="matrixsplit">
|
||||
<h1><a href="ja-JP/index.html#matrixsplit">行列による曲線の分割</a></h1>
|
||||
<p>曲線分割には、ベジエ曲線の行列表現を利用する方法もあります。<a href="#matrix">行列についての節</a>では、行列の乗算で曲線が表現できることを確認しました。特に2次・3次のベジエ曲線に関しては、それぞれ以下のような形になりました(読みやすさのため、ベジエの係数ベクトルを反転させています)。</p>
|
||||
<script>console.log("LaTeX for 77a11d65d7cffc4b84a85c4bec837792 failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/matrixsplit/77a11d65d7cffc4b84a85c4bec837792.svg" width="263px" height="55px" loading="lazy">
|
||||
<p>ならびに</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/matrixsplit/c58330e12d25c678b593ddbd4afa7c52.svg" width="323px" height="73px" loading="lazy">
|
||||
<p>曲線をある点<code>t = z</code>で分割し、新しく2つの(自明ですが、より短い)ベジエ曲線を作ることを考えましょう。曲線の行列表現と線形代数を利用すると、この2つのベジエ曲線の座標を求めることができます。まず、実際の「曲線上の点」の情報を分解し、新しい行列の積のかたちにします。</p>
|
||||
@@ -1268,8 +1268,8 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
</section>
|
||||
<section id="yforx">
|
||||
<h1><a href="ja-JP/index.html#yforx">Finding Y, given X</a></h1>
|
||||
<p>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 <em>is</em> possible and as long as you have some code in place to help, it's not a lot of a work either.</p>
|
||||
<p>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 <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<p>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 <em>is</em> possible and as long as you have some code in place to help, it's not a lot of a work either.</p>
|
||||
<p>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 <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\a6f705eb306c43e5709970b2ccad9d20.png" loading="lazy">
|
||||
@@ -1288,7 +1288,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they all cancel out, so the only one we actually need to subtract is the one at the end. Handy! So now we just solve this equation using Cardano's algorithm, and we're left with some rather short code:</p>
|
||||
<pre><code>// 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:
|
||||
@@ -1392,8 +1392,8 @@ y = curve.get(t).y</code></pre>
|
||||
<p>For instance, we can start by ensuring that the two curves share an end coordinate, so that there is no "gap" between the end of one and the start of the next curve, but that won't guarantee that things look right: both curves can be going in wildly different directions, and the resulting joined geometry will have a corner in it, rather than a smooth transition from one curve to the next.</p>
|
||||
<p>What we want is to ensure that the <a href="https://en.wikipedia.org/wiki/Curvature">curvature</a> at the transition from one curve to the next "looks good". So, we start with a shared coordinate, and then also require that derivatives for both curves match at that coordinate. That way, we're assured that their tangents line up, which must mean the curve transition is perfectly smooth. We can even make the second, third, etc. derivatives match up for better and better transitions.</p>
|
||||
<p>Problem solved!</p>
|
||||
<p>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 <a href="#reordering">reordering curves</a> 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 <em>widly</em> different derivative values.</p>
|
||||
<p>So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on something that is invariant to the <em>kind</em> 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 <em>the same</em> function for "coordinate at some distance D along the curve".</p>
|
||||
<p>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 <a href="#reordering">reordering curves</a> 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 <em>widly</em> different derivative values.</p>
|
||||
<p>So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on something that is invariant to the <em>kind</em> 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 <em>the same</em> function for "coordinate at some distance D along the curve".</p>
|
||||
<p>We've seen this before... that's the arc length function.</p>
|
||||
<p>So you might think that in order to find the curvature of a curve, we now need to solve the arc length function itself, and that this would be quite a problem because we just saw that there is no way to actually do that. Thankfully, we don't. We only need to know the <em>form</em> of the arc length function, which we saw above and is fairly simple, rather than needing to <em>solve</em> the arc length function. If we start with the arc length expression and the <a href="http://mathworld.wolfram.com/Curvature.html">run through the steps necessary</a> to determine <em>its</em> derivative (with an alternative, shorter demonstration of how to do this found <a href="https://math.stackexchange.com/a/275324/71940">over on Stackexchange</a>), then the integral that was giving us so much problems in solving the arc length function disappears entirely (because of the <a href="https://en.wikipedia.org/wiki/Fundamental_theorem_of_calculus">fundamental theorem of calculus</a>), and what we're left with us some surprisingly simple maths that relates curvature (denoted as κ, "kappa") to—and this is the truly surprising bit—a specific combination of derivatives of our original function.</p>
|
||||
<p>Let me highlight what just happened, because it's pretty special:</p>
|
||||
@@ -1401,7 +1401,7 @@ y = curve.get(t).y</code></pre>
|
||||
<li>we wanted to make curves line up, and initially thought to match the curves' derivatives, but</li>
|
||||
<li>that turned out to be a really bad choice, so instead</li>
|
||||
<li>we picked a function that is basically impossible to work with, and then <em>worked with that</em>, which</li>
|
||||
<li>gives us a simple formula that is based on <em>the curves' derivatives</em>.</li>
|
||||
<li>gives us a simple formula that is <em>and expression using the curves' derivatives</em>.</li>
|
||||
</ol>
|
||||
<p><em>That's crazy!</em></p>
|
||||
<p>But that's also one of the things that makes maths so powerful: even if your initial ideas are off the mark, you might be much closer than you thought you were, and the journey from "thinking we're completely wrong" to "actually being remarkably close to being right" is where we can find a lot of insight.</p>
|
||||
@@ -1422,7 +1422,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.</p>
|
||||
<graphics-element title="Matching curvatures for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\a98d37a0653461ad4e6065d8277c8834.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\5fcfb0572cae06717506c84768aa568c.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1431,7 +1431,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:</p>
|
||||
<graphics-element title="(Easier) curvature matching for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" data-omni="true">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\1b2e086966d7e8088e4b51a11d9ec063.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\876d7b2750d7c29068ac6181c3634d25.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control">
|
||||
@@ -1442,16 +1442,25 @@ y = curve.get(t).y</code></pre>
|
||||
<h1><a href="ja-JP/index.html#tracing">Tracing a curve at fixed distance intervals</a></h1>
|
||||
<p>Say you want to draw a curve with a dashed line, rather than a solid line, or you want to move something along the curve at fixed distance intervals over time, like a train along a track, and you want to use Bézier curves.</p>
|
||||
<p>Now you have a problem.</p>
|
||||
<p>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 <em>t</em> 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 "<em>t</em> value", by plotting them against one another.</p>
|
||||
<p>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-<em>t</em> 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).</p>
|
||||
<Graphic title="The t-for-distance function" setup={this.setup} draw={this.plotOnly}/>
|
||||
<p>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 <code>t</code> 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 "<code>t</code> value", by plotting them against one another.</p>
|
||||
<p>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.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\4f2cd306ec6fa0340ac7f410744b3118.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>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 <em>t</em>, which means we also can't compute the true inverted function that gives <em>t</em> as an expression of length. So how do we fix this?</p>
|
||||
<p>One way is to do what the graphic does: simply run through the curve, determine its <em>t</em>-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 <em>t</em> value, given a distance along the curve. This works quite well, actually, and is fairly fast.</p>
|
||||
<p>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 <em>t</em> 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:</p>
|
||||
<Graphic title="Fixed-interval coloring a curve" setup={this.setup} draw={this.drawColoured} onKeyDown={this.props.onKeyDown}/>
|
||||
<p>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 <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and then we find "the closest <code>t</code> 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 <code>t</code> 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.</p>
|
||||
<p>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 <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\25e9697557129c651e9c7cc4e4878b16.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<p>Use your up and down arrow keys to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||||
<p>Use the slider to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||||
<p>However, are there better ways? One such way is discussed in "<a href="http://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf">Moving Along a Curve with Specified Speed</a>" 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).</p>
|
||||
|
||||
</section>
|
||||
@@ -1670,7 +1679,7 @@ with quadratic or cubic curves:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/curvefitting/d84d1c71a3ce1918f53eaf8f9fe98ac4.svg" width="168px" height="27px" loading="lazy">
|
||||
<p>Here, the "to the power negative one" is the notation for the <a href="https://en.wikipedia.org/wiki/Invertible_matrix">matrix inverse</a>. But that's all we have to do: we're done. Starting with <strong>P</strong> and inventing some <code>t</code> values based on the polygon the coordinates in <strong>P</strong> define, we can compute the corresponding Bézier coordinates <strong>C</strong> that specify a curve that goes through our points. Or, if it can't go through them exactly, as near as possible.</p>
|
||||
<p>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.</p>
|
||||
<p>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!</p>
|
||||
<p>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!</p>
|
||||
<div class="figure">
|
||||
<Graphic title="Fitting a Bézier curve" setup={this.setup} draw={this.draw} onClick={this.onClick}>
|
||||
<button onClick={this.toggle} style="position:absolute; right: 0;">toggle</button>
|
||||
@@ -1942,7 +1951,7 @@ with quadratic or cubic curves:</p>
|
||||
<p>which we can then substitute in the expression for <em>a</em>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/ef3ab62bb896019c6157c85aae5d1ed3.svg" width="231px" height="195px" loading="lazy">
|
||||
<p>A quick check shows that plugging these values for <em>a</em> and <em>b</em> into the expressions for C<sub>x</sub> and C<sub>y</sub> give the same x/y coordinates for both "<em>a</em> away from A" and "<em>b</em> away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for <em>t=0.5</em> (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:</p>
|
||||
<script>console.log("LaTeX for fe32474b4616ee9478e1308308f1b6bf failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/fe32474b4616ee9478e1308308f1b6bf.svg" width="188px" height="32px" loading="lazy">
|
||||
<p>We compute T, observing that if <em>t=0.5</em>, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/e1059e611aa1e51db41f9ce0b4ebb95a.svg" width="252px" height="35px" loading="lazy">
|
||||
<p>Which, worked out for the x and y components, gives:</p>
|
||||
|
@@ -26,12 +26,6 @@ class GraphicsAPI extends BaseAPI {
|
||||
`CENTER`,
|
||||
`LEFT`,
|
||||
`RIGHT`,
|
||||
`HATCH1`,
|
||||
`HATCH2`,
|
||||
`HATCH3`,
|
||||
`HATCH4`,
|
||||
`HATCH5`,
|
||||
`HATCH6`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -608,7 +602,7 @@ class GraphicsAPI extends BaseAPI {
|
||||
/**
|
||||
* convenient axis drawing function
|
||||
*
|
||||
* api.drawAxes(pad, "t",0,1, "S","0%","100%");
|
||||
* api.drawAxes("t",0,1, "S","0%","100%");
|
||||
*
|
||||
*/
|
||||
drawAxes(hlabel, hs, he, vlabel, vs, ve, w, h) {
|
||||
@@ -619,12 +613,12 @@ class GraphicsAPI extends BaseAPI {
|
||||
this.line(0, 0, 0, h);
|
||||
|
||||
const hpos = 0 - 5;
|
||||
this.text(`${hlabel} →`, this.width / 2, hpos, this.CENTER);
|
||||
this.text(`${hlabel} →`, w / 2, hpos, this.CENTER);
|
||||
this.text(hs, 0, hpos, this.CENTER);
|
||||
this.text(he, w, hpos, this.CENTER);
|
||||
|
||||
const vpos = -10;
|
||||
this.text(`${vlabel}\n↓`, vpos, this.height / 2, this.RIGHT);
|
||||
this.text(`${vlabel}\n↓`, vpos, h / 2, this.RIGHT);
|
||||
this.text(vs, vpos, 0 + 5, this.RIGHT);
|
||||
this.text(ve, vpos, h, this.RIGHT);
|
||||
}
|
||||
|
@@ -6,14 +6,29 @@ import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
|
||||
*/
|
||||
class Bezier extends Original {
|
||||
static defaultQuadratic(apiInstance) {
|
||||
if (!apiInstance) {
|
||||
throw new Error(
|
||||
`missing reference of API instance in Bezier.defaultQuadratic(instance)`
|
||||
);
|
||||
}
|
||||
return new Bezier(apiInstance, 70, 250, 20, 110, 220, 60);
|
||||
}
|
||||
|
||||
static defaultCubic(apiInstance) {
|
||||
if (!apiInstance) {
|
||||
throw new Error(
|
||||
`missing reference of API instance in Bezier.defaultCubic(instance)`
|
||||
);
|
||||
}
|
||||
return new Bezier(apiInstance, 110, 150, 25, 190, 210, 250, 210, 30);
|
||||
}
|
||||
|
||||
constructor(apiInstance, ...coords) {
|
||||
if (!apiInstance || !apiInstance.setMovable) {
|
||||
throw new Error(
|
||||
`missing reference of API instance in Bezier constructor`
|
||||
);
|
||||
}
|
||||
super(...coords);
|
||||
this.api = apiInstance;
|
||||
this.ctx = apiInstance.ctx;
|
||||
@@ -74,7 +89,7 @@ class Bezier extends Original {
|
||||
drawCurve(color = `#333`) {
|
||||
const ctx = this.ctx;
|
||||
ctx.cacheStyle();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.beginPath();
|
||||
const lut = this.getLUT().slice();
|
||||
|
@@ -349,7 +349,7 @@ function Bezier(3,t):
|
||||
|
||||
<p>上面有一张是15<sup>th</sup>阶的插值方程。如你所见,在所有控制点中,起点和终点对曲线形状的贡献比其他点更大些。</p>
|
||||
<p>如果我们要改变曲线,就需要改变每个点的权重,有效地改变插值。可以很直接地做到这个:只要用一个值乘以每个点,来改变它的强度。这个值照惯例称为“权重”,我们可以将它加入我们原始的贝塞尔函数:</p>
|
||||
<script>console.log("LaTeX for 14cb9fbbaae9e7d87ae6bef3ea7a782e failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/control/14cb9fbbaae9e7d87ae6bef3ea7a782e.svg" width="379px" height="56px" loading="lazy">
|
||||
<p>看起来很复杂,但实际上“权重”只是我们想让曲线所拥有的坐标值:对于一条n<sup>th</sup>阶曲线,w<sup>0</sup>是起始坐标,w<sup>n</sup>是终点坐标,中间的所有点都是控制点坐标。假设说一条曲线的起点为(120,160),终点为(220,40),并受点(35,200)和点(220,260)的控制,贝塞尔曲线方程就为:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/control/c0d4dbc07b8ec7c0a18ea43c8a386935.svg" width="476px" height="40px" loading="lazy">
|
||||
<p>这就是我们在文章开头看到的曲线:</p>
|
||||
@@ -628,7 +628,7 @@ function drawCurve(points[], t):
|
||||
<section id="matrixsplit">
|
||||
<h1><a href="zh-CN/index.html#matrixsplit">Splitting curves using matrices</a></h1>
|
||||
<p>Another way to split curves is to exploit the matrix representation of a Bézier curve. In <a href="#matrix">the section on matrices</a>, we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic and cubic curves respectively: (we'll reverse the Bézier coefficients vector for legibility)</p>
|
||||
<script>console.log("LaTeX for 77a11d65d7cffc4b84a85c4bec837792 failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/matrixsplit/77a11d65d7cffc4b84a85c4bec837792.svg" width="263px" height="55px" loading="lazy">
|
||||
<p>and</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/matrixsplit/c58330e12d25c678b593ddbd4afa7c52.svg" width="323px" height="73px" loading="lazy">
|
||||
<p>Let's say we want to split the curve at some point <code>t = z</code>, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we separate out the actual "point on the curve" information into a new matrix multiplication:</p>
|
||||
@@ -1262,8 +1262,8 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
</section>
|
||||
<section id="yforx">
|
||||
<h1><a href="zh-CN/index.html#yforx">Finding Y, given X</a></h1>
|
||||
<p>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 <em>is</em> possible and as long as you have some code in place to help, it's not a lot of a work either.</p>
|
||||
<p>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 <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<p>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 <em>is</em> possible and as long as you have some code in place to help, it's not a lot of a work either.</p>
|
||||
<p>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 <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\a6f705eb306c43e5709970b2ccad9d20.png" loading="lazy">
|
||||
@@ -1282,7 +1282,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they all cancel out, so the only one we actually need to subtract is the one at the end. Handy! So now we just solve this equation using Cardano's algorithm, and we're left with some rather short code:</p>
|
||||
<pre><code>// 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:
|
||||
@@ -1386,8 +1386,8 @@ y = curve.get(t).y</code></pre>
|
||||
<p>For instance, we can start by ensuring that the two curves share an end coordinate, so that there is no "gap" between the end of one and the start of the next curve, but that won't guarantee that things look right: both curves can be going in wildly different directions, and the resulting joined geometry will have a corner in it, rather than a smooth transition from one curve to the next.</p>
|
||||
<p>What we want is to ensure that the <a href="https://en.wikipedia.org/wiki/Curvature">curvature</a> at the transition from one curve to the next "looks good". So, we start with a shared coordinate, and then also require that derivatives for both curves match at that coordinate. That way, we're assured that their tangents line up, which must mean the curve transition is perfectly smooth. We can even make the second, third, etc. derivatives match up for better and better transitions.</p>
|
||||
<p>Problem solved!</p>
|
||||
<p>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 <a href="#reordering">reordering curves</a> 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 <em>widly</em> different derivative values.</p>
|
||||
<p>So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on something that is invariant to the <em>kind</em> 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 <em>the same</em> function for "coordinate at some distance D along the curve".</p>
|
||||
<p>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 <a href="#reordering">reordering curves</a> 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 <em>widly</em> different derivative values.</p>
|
||||
<p>So what we really want is some kind of expression that's not based on any particular expression of <code>t</code>, but is based on something that is invariant to the <em>kind</em> 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 <em>the same</em> function for "coordinate at some distance D along the curve".</p>
|
||||
<p>We've seen this before... that's the arc length function.</p>
|
||||
<p>So you might think that in order to find the curvature of a curve, we now need to solve the arc length function itself, and that this would be quite a problem because we just saw that there is no way to actually do that. Thankfully, we don't. We only need to know the <em>form</em> of the arc length function, which we saw above and is fairly simple, rather than needing to <em>solve</em> the arc length function. If we start with the arc length expression and the <a href="http://mathworld.wolfram.com/Curvature.html">run through the steps necessary</a> to determine <em>its</em> derivative (with an alternative, shorter demonstration of how to do this found <a href="https://math.stackexchange.com/a/275324/71940">over on Stackexchange</a>), then the integral that was giving us so much problems in solving the arc length function disappears entirely (because of the <a href="https://en.wikipedia.org/wiki/Fundamental_theorem_of_calculus">fundamental theorem of calculus</a>), and what we're left with us some surprisingly simple maths that relates curvature (denoted as κ, "kappa") to—and this is the truly surprising bit—a specific combination of derivatives of our original function.</p>
|
||||
<p>Let me highlight what just happened, because it's pretty special:</p>
|
||||
@@ -1395,7 +1395,7 @@ y = curve.get(t).y</code></pre>
|
||||
<li>we wanted to make curves line up, and initially thought to match the curves' derivatives, but</li>
|
||||
<li>that turned out to be a really bad choice, so instead</li>
|
||||
<li>we picked a function that is basically impossible to work with, and then <em>worked with that</em>, which</li>
|
||||
<li>gives us a simple formula that is based on <em>the curves' derivatives</em>.</li>
|
||||
<li>gives us a simple formula that is <em>and expression using the curves' derivatives</em>.</li>
|
||||
</ol>
|
||||
<p><em>That's crazy!</em></p>
|
||||
<p>But that's also one of the things that makes maths so powerful: even if your initial ideas are off the mark, you might be much closer than you thought you were, and the journey from "thinking we're completely wrong" to "actually being remarkably close to being right" is where we can find a lot of insight.</p>
|
||||
@@ -1416,7 +1416,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.</p>
|
||||
<graphics-element title="Matching curvatures for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\a98d37a0653461ad4e6065d8277c8834.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\5fcfb0572cae06717506c84768aa568c.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1425,7 +1425,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:</p>
|
||||
<graphics-element title="(Easier) curvature matching for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" data-omni="true">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\1b2e086966d7e8088e4b51a11d9ec063.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\876d7b2750d7c29068ac6181c3634d25.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control">
|
||||
@@ -1436,16 +1436,25 @@ y = curve.get(t).y</code></pre>
|
||||
<h1><a href="zh-CN/index.html#tracing">Tracing a curve at fixed distance intervals</a></h1>
|
||||
<p>Say you want to draw a curve with a dashed line, rather than a solid line, or you want to move something along the curve at fixed distance intervals over time, like a train along a track, and you want to use Bézier curves.</p>
|
||||
<p>Now you have a problem.</p>
|
||||
<p>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 <em>t</em> 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 "<em>t</em> value", by plotting them against one another.</p>
|
||||
<p>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-<em>t</em> 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).</p>
|
||||
<Graphic title="The t-for-distance function" setup={this.setup} draw={this.plotOnly}/>
|
||||
<p>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 <code>t</code> 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 "<code>t</code> value", by plotting them against one another.</p>
|
||||
<p>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.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\4f2cd306ec6fa0340ac7f410744b3118.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>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 <em>t</em>, which means we also can't compute the true inverted function that gives <em>t</em> as an expression of length. So how do we fix this?</p>
|
||||
<p>One way is to do what the graphic does: simply run through the curve, determine its <em>t</em>-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 <em>t</em> value, given a distance along the curve. This works quite well, actually, and is fairly fast.</p>
|
||||
<p>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 <em>t</em> 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:</p>
|
||||
<Graphic title="Fixed-interval coloring a curve" setup={this.setup} draw={this.drawColoured} onKeyDown={this.props.onKeyDown}/>
|
||||
<p>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 <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and then we find "the closest <code>t</code> 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 <code>t</code> 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.</p>
|
||||
<p>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 <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\25e9697557129c651e9c7cc4e4878b16.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<p>Use your up and down arrow keys to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||||
<p>Use the slider to increase or decrease the number of equidistant segments used to colour the curve.</p>
|
||||
<p>However, are there better ways? One such way is discussed in "<a href="http://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf">Moving Along a Curve with Specified Speed</a>" 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).</p>
|
||||
|
||||
</section>
|
||||
@@ -1664,7 +1673,7 @@ with quadratic or cubic curves:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/curvefitting/d84d1c71a3ce1918f53eaf8f9fe98ac4.svg" width="168px" height="27px" loading="lazy">
|
||||
<p>Here, the "to the power negative one" is the notation for the <a href="https://en.wikipedia.org/wiki/Invertible_matrix">matrix inverse</a>. But that's all we have to do: we're done. Starting with <strong>P</strong> and inventing some <code>t</code> values based on the polygon the coordinates in <strong>P</strong> define, we can compute the corresponding Bézier coordinates <strong>C</strong> that specify a curve that goes through our points. Or, if it can't go through them exactly, as near as possible.</p>
|
||||
<p>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.</p>
|
||||
<p>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!</p>
|
||||
<p>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!</p>
|
||||
<div class="figure">
|
||||
<Graphic title="Fitting a Bézier curve" setup={this.setup} draw={this.draw} onClick={this.onClick}>
|
||||
<button onClick={this.toggle} style="position:absolute; right: 0;">toggle</button>
|
||||
@@ -1936,7 +1945,7 @@ with quadratic or cubic curves:</p>
|
||||
<p>which we can then substitute in the expression for <em>a</em>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/ef3ab62bb896019c6157c85aae5d1ed3.svg" width="231px" height="195px" loading="lazy">
|
||||
<p>A quick check shows that plugging these values for <em>a</em> and <em>b</em> into the expressions for C<sub>x</sub> and C<sub>y</sub> give the same x/y coordinates for both "<em>a</em> away from A" and "<em>b</em> away from B", so let's continue: now that we know the coordinate values for C, we know where our on-curve point T for <em>t=0.5</em> (or angle φ/2) is, because we can just evaluate the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:</p>
|
||||
<script>console.log("LaTeX for fe32474b4616ee9478e1308308f1b6bf failed!");</script>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/fe32474b4616ee9478e1308308f1b6bf.svg" width="188px" height="32px" loading="lazy">
|
||||
<p>We compute T, observing that if <em>t=0.5</em>, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/circles/e1059e611aa1e51db41f9ce0b4ebb95a.svg" width="252px" height="35px" loading="lazy">
|
||||
<p>Which, worked out for the x and y components, gives:</p>
|
||||
|