1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-31 03:59:58 +02:00

up to and including section 21

This commit is contained in:
Pomax
2016-01-04 16:17:01 -08:00
parent 8b78e22ac4
commit 76fc269b07
37 changed files with 5576 additions and 3407 deletions

View File

@@ -331,12 +331,15 @@ var Graphic = React.createClass({
},
drawFunction: function(generator, offset) {
var p0 = generator(0),
plast = generator(1),
var start = generator.start || 0,
p0 = generator(start),
end = generator.end || 1,
plast = generator(end),
step = generator.step || 0.01,
scale = generator.scale || 1,
p, t;
for (t=step; t<1.0; t+=step) {
p = generator(t);
for (t=step; t<end; t+=step) {
p = generator(t, scale);
this.drawLine(p0, p, offset);
p0 = p;
}
@@ -426,7 +429,16 @@ var Graphic = React.createClass({
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.rect(p1.x, p1.y, p2.x-p1.x, p2.y-p1.y);
var x = p1.x + ox,
y = p1.y + oy,
w = p2.x-p1.x,
h = p2.y-p1.y;
this.ctx.beginPath();
this.ctx.moveTo(x,y);
this.ctx.lineTo(x+w, y);
this.ctx.lineTo(x+w, y+h);
this.ctx.lineTo(x,y+h);
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
},

View File

@@ -0,0 +1,256 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var sin = Math.sin;
var tau = Math.PI*2;
var Arclength = React.createClass({
getDefaultProps: function() {
return {
title: "Arc length"
};
},
setup: function(api) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var generator;
if (!this.generator) {
generator = ((v,scale) => {
scale = scale || 1;
return {
x: v*w/tau,
y: scale * sin(v)
};
});
generator.start = 0;
generator.end = tau;
generator.step = 0.1;
generator.scale = h/3;
this.generator = generator;
}
},
drawSine: function(api, dheight) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var generator = this.generator;
generator.dheight = dheight;
api.setColor("black");
api.drawLine({x:0,y:h/2}, {x:w,y:h/2});
api.drawFunction(generator, {x:0, y:h/2});
},
drawSlices: function(api, steps) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var f = w/tau;
var area = 0;
var c = steps <= 25 ? 1 : 0;
api.reset();
api.setColor("transparent");
api.setFill("rgba(150,150,255, 0.4)");
for (var step=tau/steps, i=step/2, v, p1, p2; i<tau+step/2; i+=step) {
v = this.generator(i);
p1 = {x:v.x - f*step/2 + c, y: 0};
p2 = {x:v.x + f*step/2 - c, y: v.y * this.generator.scale};
if (!c) { api.setFill("rgba(150,150,255,"+(0.4 + 0.3*Math.random())+")"); }
api.drawRect(p1, p2, {x:0, y:h/2});
area += step * Math.abs(v.y * this.generator.scale);
}
api.setFill("black");
var trueArea = ((100 * 4 * h/3)|0)/100;
var currArea = ((100 * area)|0)/100;
api.text("Approximating with "+steps+" strips (true area: "+trueArea+"): " + currArea, {x: 10, y: h-15});
},
drawCoarseIntegral: function(api) {
api.reset();
this.drawSlices(api, 10);
this.drawSine(api);
},
drawFineIntegral: function(api) {
api.reset();
this.drawSlices(api, 24);
this.drawSine(api);
},
drawSuperFineIntegral: function(api) {
api.reset();
this.drawSlices(api, 99);
this.drawSine(api);
},
setupCurve: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
drawCurve: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var len = curve.length();
api.setFill("black");
api.text("Curve length: "+len+" pixels", {x:10, y:15});
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<p>How long is a Bézier curve? As it turns out, that's not actually an easy question, because the answer
requires maths that —much like root finding— cannot generally be solved the traditional way. If we
have a parametric curve with <i>f<sub>x</sub>(t)</i> and <i>f<sub>y</sub>(t)</i>, then the length of the
curve, measured from start point to some point <i>t = z</i>, is computed using the following seemingly
straight forward (if a bit overwhelming) formula:</p>
<p>\[
\int_{0}^{z}\sqrt{f_x'(t)^2+f_y'(t)^2} dt
\]</p>
<p>or, more commonly written using Leibnitz notation as:</p>
<p>\[
length = \int_{0}^{z}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\]</p>
<p>This formula says that the length of a parametric curve is in fact equal to the <b>area</b> underneath a function that
looks a remarkable amount like Pythagoras' rule for computing the diagonal of a straight angled triangle. This sounds
pretty simple, right? Sadly, it's far from simple... cutting straight to after the chase is over: for quadratic curves,
this formula generates an <a href="http://www.wolframalpha.com/input/?i=antiderivative+for+sqrt((2*(1-t)*t*B+%2b+t^2*C)'^2+%2b+(2*(1-t)*t*E)'^2)&incParTime=true">unwieldy computation</a>,
and we're simply not going to implement things that way. For cubic Bézier curves, things get even more fun, because there
is no "closed form" solution, meaning that due to the way calculus works, there is no generic formula that allows you to
calculate the arc length. Let me just repeat this, because it's fairly crucial: <strong><em>for cubic and higher Bézier curves,
there is no way to solve this function if you want to use it "for all possible coordinates".</em></strong></p>
<p>Seriously: <a href="https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem">It cannot be done.</a></p>
<p>So we turn to numerical approaches again. The method we'll look at here is the
<a href="http://www.youtube.com/watch?v=unWguclP-Ds&feature=BFa&list=PLC8FC40C714F5E60F&index=1">Gauss
quadrature</a>. This approximation is a really neat trick, because for any <i>n<sup>th</sup></i> degree polynomial
it finds approximated values for an integral really efficiently. Explaining this procedure in length is way beyond
the scope of this page, so if you're interested in finding out why it works, I can recommend the University of
South Florida video lecture on the procedure, linked in this very paragraph. The general solution we're looking
for is the following:</p>
<p>\[
\int_{-1}^{1}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\simeq
\left [
\underset{strip\ 1}{ \underbrace{ C_1 \cdot f\left(t_1\right) }}
\ +\ ...
\ +\ \underset{strip\ n}{ \underbrace{ C_n \cdot f\left(t_n\right) }}
\right ]
=
\underset{strips\ 1\ through\ n}{
\underbrace{
\sum_{i=1}^{n}{
C_i \cdot f\left(t_i\right)
}
}
}
\]</p>
<p>In plain text: an integral function can always be treated as the sum of an (infinite) number of
(infinitely thin) rectangular strips sitting "under" the function's plotted graph. To illustrate
this idea, the following graph shows the integral for a sinoid function. The more strips we use (and
of course the more we use, the thinner they get) the closer we get to the true area under the curve, and
thus the better the approximation:</p>
<div className="figure">
<Graphic inline={true} preset="empty" title="A function's approximated integral" setup={this.setup} draw={this.drawCoarseIntegral}/>
<Graphic inline={true} preset="empty" title="A better approximation" setup={this.setup} draw={this.drawFineIntegral}/>
<Graphic inline={true} preset="empty" title="An even better approximation" setup={this.setup} draw={this.drawSuperFineIntegral}/>
</div>
<p>Now, infinitely many terms to sum and infinitely thin rectangles are not something that computers
can work with, so instead we're going to approximate the infinite summation by using a sum of a finite
number of "just thin" rectangular strips. As long as we use a high enough number of thin enough rectangular
strips, this will give us an approximation that is pretty close to what the real value is.</p>
<p>So, the trick is to come up with useful rectangular strips. A naive way is to simply create <i>n</i> strips,
all with the same width, but there is a far better way using special values for <i>C</i> and <i>f(t)</i> depending
on the value of <i>n</i>, which indicates how many strips we'll use, and it's called the Legendre-Gauss quadrature.</p>
<p>This approach uses strips that are <em>not</em> spaced evenly, but instead spaces them in a special way that works
remarkably well. If you look at the earlier sinoid graphic, you could imagine that we could probably get a result
similar to the one with 99 strips if we used fewer strips, but spaced them so that the steeper the curve is, the
thinner we make the strip, and conversely, the flatter the curve is (especially near the tops of the function),
the wider we make the strip. That's akin to how the Legendre values work.</p>
<div className="note">
<p>Note that one requirement for the approach we'll use is that the integral must run from -1 to 1. That's no good, because
we're dealing with Bézier curves, and the length of a section of curve applies to values which run from 0 to "some
value smaller than or equal to 1" (let's call that value <i>z</i>). Thankfully, we can quite easily transform any
integral interval to any other integral interval, by shifting and scaling the inputs. Doing so, we get the
following:</p>
<p>\[\begin{array}{l}
\int_{0}^{z}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\\
\simeq \
\frac{z}{2} \cdot \left [ C_1 \cdot f\left(\frac{z}{2} \cdot t_1 + \frac{z}{2}\right)
+ ...
+ C_n \cdot f\left(\frac{z}{2} \cdot t_n + \frac{z}{2}\right)
\right ]
\\
= \
\frac{z}{2} \cdot \sum_{i=1}^{n}{C_i \cdot f\left(\frac{z}{2} \cdot t_i + \frac{z}{2}\right)}
\end{array}\]</p>
<p>That may look a bit more complicated, but the fraction involving <i>z</i> is a fixed number,
so the summation, and the evaluation of the <i>f(t)</i> values are still pretty simple.</p>
<p>So, what do we need to perform this calculation? For one, we'll need an explicit formula for <i>f(t)</i>,
because that derivative notation is handy on paper, but not when we have to implement it. We'll also
need to know what these <i>C<sub>i</sub></i> and <i>t<sub>i</sub></i> values should be. Luckily, that's
less work because there are actually many tables available that give these values, for any <i>n</i>,
so if we want to approximate our integral with only two terms (which is a bit low, really)
then <a href="legendre-gauss.html">these tables</a> would tell us that for <i>n=2</i> we must use the
following values:</p>
<p>\[\begin{array}{l}
C_1 = 1 \\
C_2 = 1 \\
t_1 = - \frac{1}{\sqrt{3}} \\
t_2 = + \frac{1}{\sqrt{3}}
\end{array}\]</p>
<p>Which means that in order for us to approximate the integral, we must plug these values into the approximate
function, which gives us:</p>
<p>\[
\int_{0}^{z}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\frac{z}{2} \cdot \left [ f\left( \frac{z}{2} \cdot \frac{-1}{\sqrt{3}} + \frac{z}{2} \right)
+ f\left( \frac{z}{2} \cdot \frac{1}{\sqrt{3}} + \frac{z}{2} \right)
\right ]
\]</p>
<p>We can program that pretty easily, provided we have that <i>f(t)</i> available, which we do,
as we know the full description for the Bézier curve functions B<sub>x</sub>(t) and B<sub>y</sub>(t).</p>
</div>
<p>If we use the Legendre-Gauss values for our <i>C</i> values (thickness for each strip) and <i>t</i> values
(location of each strip), we can determine the approximate length of a Bézier curve by computing the
Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and
change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight
line, and see if the length matches what you'd expect. What if you form a line with the control points
on the outside, and the start/end points on the inside?</p>
<Graphic preset="simple" title="Arc length for a Bézier curve" setup={this.setupCurve} draw={this.drawCurve}/>
</section>
);
}
});
module.exports = Arclength;

View File

@@ -0,0 +1,183 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var sin = Math.sin;
var tau = Math.PI*2;
var ArclengthApprox = React.createClass({
getDefaultProps: function() {
return {
title: "Approximated arc length"
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 10;
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 16;
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var pts = curve.getLUT(api.steps);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
var len = curve.length();
var alen = 0;
for(var i=0,p0,p1,dx,dy; i<pts.length-1; i++) {
p0 = pts[i];
p1 = pts[i+1];
dx = p1.x-p0.x;
dy = p1.y-p0.y;
alen += Math.sqrt(dx*dx+dy*dy);
}
alen = ((100*alen)|0)/100;
len = ((100*len)|0)/100;
api.text("Approximate length, "+api.steps+" steps: "+alen+" (true: "+len+")", {x:10, y: 15});
},
values: {
"38": 1, // up arrow
"40": -1, // down arrow
},
onKeyDown: function(e, api) {
var v = this.values[e.keyCode];
if(v) {
e.preventDefault();
api.steps += v;
if (api.steps < 1) {
api.steps = 1;
}
console.log(api.steps);
}
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<p>Sometimes, we don't actually need the precision of a true arc length, and we can get away with simply computing the
approximate arc length instead. The by far fastest way to do this is to flatten the curve and then simply calculate
the linear distance from point to point. This will come with an error, but this can be made arbitrarily small by
increasing the segment count.</p>
<p>If we combine the work done in the previous sections on curve flattening and arc length computation, we can
implement these with minimal effort:</p>
<Graphic preset="twopanel" title="Approximate quadratic curve arc length" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.onKeyDown} />
<Graphic preset="twopanel" title="Approximate cubic curve arc length" setup={this.setupCubic} draw={this.draw} onKeyDown={this.onKeyDown} />
<p>Try clicking on the sketch and using your up and down arrow keys to lower the number of segments for both
the quadratic and cubic curve. You may notice that the error in length is actually pretty significant, even if
the percentage is fairly low: if the number of segments used yields an error of 0.1% or higher, the flattened
curve already looks fairly obviously flattened. And of course, the longer the curve, the more significant the
error will be.</p>
</section>
);
}
});
module.exports = ArclengthApprox;
/*
void setupCurve() {
setupDefaultQuadratic();
offsetting();
offset = 16;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
float x = curve.getXValue(0),
y = curve.getYValue(0),
x2, y2, step = 1/offset, t,
length=0;
for(int i=1; i<=offset; i++) {
t = i*step;
x2 = curve.getXValue(t);
y2 = curve.getYValue(t);
line(x,y,x2,y2);
length += dist(x,y,x2,y2);
x = x2;
y = y2;
}
float arclength = curve.getCurveLength();
float error = 100 * (arclength - length) / arclength;
length = nfc(length, 3, 3);
arclength = nfc(arclength, 3, 3);
error = nfc(error, 3, 3);
if(error.indexOf(".")===0) { error = "0" + error; }
fill(0);
text("Approximate arc length based on "+offset+" segments: " + length, -dim/4, dim-20);
text("True length: " + arclength + ", error: " + error + "%", -dim/4, dim-5);
}</textarea>
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 24;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
float x = curve.getXValue(0),
y = curve.getYValue(0),
x2, y2, step = 1/offset, t,
length=0;
for(int i=1; i<=offset; i++) {
t = i*step;
x2 = curve.getXValue(t);
y2 = curve.getYValue(t);
line(x,y,x2,y2);
length += dist(x,y,x2,y2);
x = x2;
y = y2;
}
float arclength = curve.getCurveLength();
float error = 100 * (arclength - length) / arclength;
length = nfc(length, 3, 3);
arclength = nfc(arclength, 3, 3);
error = nfc(error, 3, 3);
if(error.indexOf(".")===0) { error = "0" + error; }
fill(0);
text("Approximate arc length based on "+offset+" segments: " + length, -dim/4, dim-20);
text("True length: " + arclength + ", error: " + error + "%", -dim/4, dim-5);
}</textarea>
*/

View File

@@ -40,10 +40,8 @@ var Flattening = React.createClass({
},
values: {
"107": 1, // numpad + key
"187": 1, // =/+ main board key
"109": -1, // numpad - key
"189": -1 // -/_ main board key
"38": 1, // up arrow
"40": -1, // down arrow
},
onKeyDown: function(e, api) {
@@ -64,12 +62,18 @@ var Flattening = React.createClass({
<p>We can also simplify the drawing process by "sampling" the curve at certain points, and then joining those points up with straight lines, a process known as "flattening", as we are reducing a curve to a simple sequence of straight, "flat" lines.</p>
<p>We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.</p>
<p>We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we
end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or
even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good
enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use
the flattened for for doing true intersection detection, or curvature alignment.</p>
<Graphic preset="twopanel" title="Flattening a quadratic curve" setup={this.setupQuadratic} draw={this.drawFlattened} onKeyDown={this.onKeyDown}/>
<Graphic preset="twopanel" title="Flattening a cubic curve" setup={this.setupCubic} draw={this.drawFlattened} onKeyDown={this.onKeyDown} />
<p>Try clicking on the sketch and using your '+' and '-' keys to lower the number of segments for both the quadratic and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.</p>
<p>Try clicking on the sketch and using your up and down arrow keys to lower the number of segments for both the quadratic
and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex
curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.</p>
<div className="howtocode">
<h3>How to implement curve flattening</h3>

View File

@@ -23,15 +23,15 @@ module.exports = {
boundingbox: require("./boundingbox"),
aligning: require("./aligning"),
tightbounds: require("./tightbounds"),
canonical: require("./canonical")
canonical: require("./canonical"),
arclength: require("./arclength"),
arclengthapprox: require("./arclengthapprox"),
tracing: require("./tracing")
};
/*
arclength: require("./arclength"),
arclengthapprox: require("./arclengthapprox"),
tracing: require("./tracing"),
intersections: require("./intersections"),
curveintersection: require("./curveintersection"),
moulding: require("./moulding"),
@@ -54,9 +54,6 @@ module.exports = {
*/
/*
Arc length
Approximated arc length
Tracing a curve at fixed distance intervals
Intersections
Curve/curve intersection
Curve moulding (using the projection ratio)

View File

@@ -0,0 +1,206 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var map = function(v, ds,de, ts,te) {
return ts + (v-ds)/(de-ds) * (te-ts);
};
var Tracing = React.createClass({
getDefaultProps: function() {
return {
title: "Tracing a curve at fixed distance intervals"
};
},
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: map(t, 0,1, 0,fwh),
y: 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);
}
},
values: {
"38": 1, // up arrow
"40": -1, // down arrow
},
onKeyDown: function(e, api) {
var v = this.values[e.keyCode];
if(v) {
e.preventDefault();
api.steps += v;
if (api.steps < 1) {
api.steps = 1;
}
console.log(api.steps);
}
},
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(var i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
ts.forEach(p => {
var pt = { x: map(p.t,0,1,0,fwh), y: 0 };
var pd = { x: 0, y: 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);
});
var 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);
api.drawCircle(curve.get(0), 4, offset);
for (var 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;
}
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<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 <i>t</i> 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
"<i>t</i> value", by plotting them against one another.</p>
<p>The following graphic shows a particularly illustrative curve, and it's length-to-<i>t</i> 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-<i>t</i> 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, able to switch concave/convex form twice
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 preset="twopanel" title="The t-for-distance function" setup={this.setup} draw={this.plotOnly}/>
<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 <i>t</i>, which means we also can't compute the true
inverted function that gives <i>t</i> 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
<i>t</i>-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 <i>t</i> 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 <i>t</i> 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 preset="threepanel" title="Fixed-interval coloring a curve" setup={this.setup} draw={this.drawColoured} onKeyDown={this.onKeyDown}/>
<p>Use your up and down arrow keys 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>
);
}
});
module.exports = Tracing;