1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-14 04:34:21 +02:00

rebuild + sections up to 14

This commit is contained in:
Pomax
2016-01-02 10:05:27 -08:00
parent 15523da391
commit c8ebcf29b9
31 changed files with 3079 additions and 94 deletions

View File

@@ -457,7 +457,27 @@ var Graphic = React.createClass({
offset.y += this.offset.y;
}
this.ctx.fillText(text, offset.x, offset.y);
},
drawAxes: function(pad, xlabel, xs, xe, ylabel, ys, ye, offset) {
offset = offset || { x:0, y:0 };
var dim = this.getPanelWidth();
this.drawLine({x:pad, y:pad}, {x:dim-pad, y:pad}, offset);
this.drawLine({x:pad, y:pad}, {x:pad, y:dim-pad}, offset);
this.setFill("black");
this.text(xlabel + " ", {x: offset.x + dim/2, y: offset.y + 15});
this.text(xs, {x: offset.x + pad, y: offset.y + 15});
this.text(xe, {x: offset.x + dim - pad, y: offset.y + 15});
this.text(ylabel, {x: offset.x + 5, y: offset.y + dim/2 - pad});
this.text("", {x: offset.x + 5, y: offset.y + dim/2});
this.text(ys, {x: offset.x + 4, y: offset.y + pad + 5});
this.text(ye, {x: offset.x + 2, y: offset.y + dim - pad + 10});
}
});
module.exports = Graphic;

View File

@@ -0,0 +1,82 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var Components = React.createClass({
statics: {
title: "Component functions"
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[2].x = 210;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.setPanelCount(3);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var tf = curve.order + 1,
pad = 20,
pts = curve.points,
w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = { x: w, y: 0 };
var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; });
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "x",0,w, offset);
offset.x += pad;
api.drawCurve(new api.Bezier(x_pts), offset);
offset.x += w-pad;
var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; });
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "y",0,w, offset);
offset.x += pad;
api.drawCurve(new api.Bezier(y_pts), offset);
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ Components.title }</SectionHeader>
<p>One of the first things people run into when they start using Bézier curves in their own programs is
"I know how to draw the curve, but how do I determine the bounding box?". It's actually reasonably straight
forward to do so, but it requires having some knowledge on exploiting math to get the values we need.
For bounding boxes, we aren't actually interested in the curve itself, but only in its "extremities": the
minimum and maximum values the curve has for its x- and y-axis values. If you remember your calculus
(provided you ever took calculus, otherwise it's going to be hard to remember) we can determine function
extremities using the first derivative of that function, but this poses a problem, since our function is
parametric: every axis has its own function.</p>
<p>The solution: compute the derivative for each axis separately, and then fit them back together in the same
way we do for the original.</p>
<p>Let's look at how a parametric Bézier curve "splits up" into two normal functions, one for the x-axis and
one for the y-axis. Note the left-most figure is again an interactive curve, without labeled axes (you
get coordinates in the graph instead). The center and right-most figures are the component functions for
computing the x-axis value, given a value for <i>t</i> (between 0 and 1 inclusive), and the y-axis value,
respectively.</p>
<p>If you move points in a curve sideways, you should only see the middle graph change; likely, moving
points vertically should only show a change in the right graph.</p>
<Graphic preset="simple" title="Quadratic Bézier curve components" setup={this.setupQuadratic} draw={this.draw}/>
<Graphic preset="simple" title="Cubic Bézier curve components" setup={this.setupCubic} draw={this.draw}/>
</section>
);
}
});
module.exports = Components;

View File

@@ -18,22 +18,6 @@ var Control = React.createClass({
api.drawCurve(curve);
},
drawGraphBasics: function(api, dim, pad, fwh) {
api.drawLine({x:pad, y:pad}, {x:dim-pad, y:pad});
api.drawLine({x:pad, y:pad}, {x:pad, y:dim-pad});
api.setFill("black");
api.text("t →", {x:dim/2, y: 15});
api.text("0", {x:pad, y: 15});
api.text("1", {x:dim-pad, y: 15});
api.text("S", {x:5, y: dim/2-pad});
api.text("↓", {x:5, y: dim/2});
api.text("0%", {x:4, y: pad+5});
api.text("100%", {x:2, y: dim-pad+10});
},
drawFunction: function(api, label, where, generator) {
api.setRandomColor();
api.drawFunction(generator);
@@ -64,7 +48,8 @@ var Control = React.createClass({
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
this.drawGraphBasics(api, dim, pad, fwh);
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var p = api.hover;
if (p && p.x >= pad && p.x <= dim-pad) {
@@ -101,7 +86,8 @@ var Control = React.createClass({
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
this.drawGraphBasics(api, dim, pad, fwh);
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var p = api.hover;
if (p && p.x >= pad && p.x <= dim-pad) {
@@ -145,7 +131,8 @@ var Control = React.createClass({
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
this.drawGraphBasics(api, dim, pad, fwh);
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var factors = [1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1]
@@ -246,7 +233,7 @@ var Control = React.createClass({
<pre>function Bezier(n,t,w[]):
sum = 0
for(k=0; k&lt;n; k++):
for(k=0; k<n; k++):
sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
return sum</pre>

View File

@@ -78,7 +78,7 @@ var deCasteljau = React.createClass({
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
for(i=0; i<newpoints.length; i++):
newpoints[i] = (1-t) * points[i] + t * points[i+1]
drawCurve(newpoints, t)</pre>
@@ -91,7 +91,7 @@ var deCasteljau = React.createClass({
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
for(i=0; i<newpoints.length; i++):
x = (1-t) * points[i].x + t * points[i+1].x
y = (1-t) * points[i].y + t * points[i+1].y
newpoints[i] = new point(x,y)

View File

@@ -155,7 +155,7 @@ var Explanation = React.createClass({
<pre>function Bezier(n,t):
sum = 0
for(k=0; k&lt;n; k++):
for(k=0; k<n; k++):
sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
return sum</pre>
@@ -195,7 +195,7 @@ binomial(n,k):
<pre>function Bezier(n,t):
sum = 0
for(k=0; k&lt;n; k++):
for(k=0; k<n; k++):
sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
return sum</pre>

View File

@@ -0,0 +1,251 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var Extremities = React.createClass({
statics: {
title: "Component functions"
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[2].x = 210;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.setPanelCount(3);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var tf = curve.order + 1,
pad = 20,
pts = curve.points,
w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = { x: w, y: 0 },
extremities;
var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; });
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "x",0,w, offset);
offset.x += pad;
var xcurve = new api.Bezier(x_pts);
api.drawCurve(xcurve, offset);
api.setColor("red");
xcurve.inflections().y.forEach(t => {
var p = xcurve.get(t);
api.drawCircle(p, 3, offset);
});
offset.x += w-pad;
var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; });
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "y",0,w, offset);
offset.x += pad;
var ycurve = new api.Bezier(y_pts);
api.drawCurve(ycurve, offset);
api.setColor("red");
ycurve.inflections().y.forEach(t => {
var p = ycurve.get(t);
api.drawCircle(p, 3, offset);
});
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ Extremities.title }</SectionHeader>
<p>Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our
Bézier curve by finding maxima and minima on the component functions, by solving the equations B'(t) = 0 and B''(t) = 0.
Although, in the case of quadratic curves there is no B''(t), so we only need to compute B'(t) = 0. So, how do we compute the first and second derivatives? Fairly easily, actually, until our derivatives are 4th order or higher... then things get really hard. But let's start simple:</p>
<h3>Quadratic curves: linear derivatives.</h3>
<p>Finding the solution for "where is this line 0" should be trivial:</p>
<p>\[\begin{align}
l(x) = ax + b &= 0,\\
ax + b &= 0,\\
ax &= -b \\
x &= \frac{-b}{a}
\end{align}\]</p>
<p>Done. And quadratic curves have no meaningful second derivative, so we're <em>really</em> done.</p>
<h3>Cubic curves: the quadratic formula.</h3>
<p>The derivative of a cubic curve is a quadratic curve, and finding the roots for a quadratic Bézier curve means we can apply the <a href="https://en.wikipedia.org/wiki/Quadratic_formula">Quadratic formulat</a>. If you've seen it before, you'll remember it, and if you haven't, it looks like this:</p>
<p>\[
Given\ f(t) = at^2 + bt + c,\ f(t)=0\ when\ t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
\]</p>
<p>So, if we can express a Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that come out (because of that plus/minus sign we get two). Any value between 0 and 1 is a root that matters for Bézier curves, anything below or above that is irrelevant (because Bézier curves are only defined over the interval [0,1]). So, how do we convert?</p>
<p>First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the <a href="#derivatives">derivatives section</a>:</p>
<p>\[
B(t)\ uses\ \{ p_1,p_2,p_3,p_4 \} \\
B'(t)\ uses\ \{ v_1.v_2,v_3 \},\ where\ v_1 = 3(p_2-p_1),\ v_2 = 3(p_3-p_2),\ v_3 = 3(p_4-p_3)
\]</p>
<p>And then, using these <em>v</em> values, we can find out what our <em>a</em>, <em>b</em>, and <em>c</em> should be:</p>
<p>\[\begin{align}
B'(t) &= v_1(1-t)^2 + 2v_2(1-t)t + v_3t^2 \\
... &= v_1(t^2 - 2t + 1) + 2v_2(t-t^2) + v_3t^2 \\
... &= v_1t^2 - 2v_1t + v_1 + 2v_2t - 2v_2t^2 + v_3t^2 \\
... &= v_1t^2 - 2v_2t^2 + v_3t^2 - 2v_1t + v_1 + 2v_2t \\
... &= (v_1-2v_2+v_3)t^2 + 2(v_2-v_1)t + v_1
\end{align}\]</p>
<p>So we can find the roots by using:</p>
<p>\[\begin{align}
a &= v_1-2v_2+v_3 = 3(-p_1 + 3p_2 - 3p_3 + p_4) \\
b &= 2(v_2-v_1) = 6(p_1 - 2p_2 + p_3) \\
c &= v_1 = 3(p_2-p_1)
\end{align}\]</p>
<p>Easy peasy. We also note that the second derivative of a cubic curve means computing the first derivative of a quadratic curve, and we just saw how to do that in the section above.</p>
<h3>Quartic curves: Cardano's algorithm.</h3>
<p>Quarticfourth degreecurves have a cubic function as derivative. Now, cubic functions are a bit of a problem because they're really hard to solve. But, way back in the 16<sup>th</sup> century, <a href="https://en.wikipedia.org/wiki/Gerolamo_Cardano">Gerolamo Cardano</a> figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots is "easy", and then the only hard part is figuring out how to go from that form to the generic form. So:</p>
<p>\[
very\ hard:\ solve\ at^3 + bt^2 + ct + d = 0\\
easier:\ solve\ t^3 + pt + q = 0
\]</p>
<p>This is easier because for the "easier formula" we can use <a href="http://www.wolframalpha.com/input/?i=t^3+%2B+pt+%2B+q">regular calculus</a> to find the roots (as a cubic function, however, it can have up to three roots, but two of those can be complex. For the purpose of Bézier curve extremities, we can completely ignore those complex roots, since our <em>t</em> is a plain real number from 0 to 1).</p>
<p>So, the trick is to figure out how to turn the first formula into the second formula, and to then work out the maths that gives us the roots. This is explained in detail over at <a href="http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm">Ken J. Ward's page</a> for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic equation, with the complex roots getting totally ignored.</p>
<div className="note"><pre>
// A helper function to filter for values in the [0,1] interval:
function accept(t) {
return 0<=t && t <=1;
}
// A special cuberoot function, which we can use because we don't care about complex roots:
function crt(v) {
if(v<0) return -Math.pow(-v,1/3);
return Math.pow(v,1/3);
}
// Now then: given cubic coordinates pa, pb, pc, pd, find all roots.
function getCubicRoots(pa, pb, pc, pd) {
var d = (-pa + 3*pb - 3*pc + pd),
a = (3*pa - 6*pb + 3*pc) / d,
b = (-3*pa + 3*pb) / d,
c = pa / d;
var p = (3*b - a*a)/3,
p3 = p/3,
q = (2*a*a*a - 9*a*b + 27*c)/27,
q2 = q/2,
discriminant = q2*q2 + p3*p3*p3;
// and some variables we're going to use later on:
var u1,v1,root1,root2,root3;
// three possible real roots:
if (discriminant < 0) {
var mp3 = -p/3,
mp33 = mp3*mp3*mp3,
r = sqrt( mp33 ),
t = -q / (2*r),
cosphi = t<-1 ? -1 : t>1 ? 1 : t,
phi = acos(cosphi),
crtr = cuberoot(r),
t1 = 2*crtr;
root1 = t1 * cos(phi/3) - a/3;
root2 = t1 * cos((phi+2*pi)/3) - a/3;
root3 = t1 * cos((phi+4*pi)/3) - a/3;
return [root1, root2, root3].filter(accept);
}
// three real roots, but two of them are equal:
else if(discriminant === 0) {
u1 = q2 < 0 ? cuberoot(-q2) : -cuberoot(q2);
root1 = 2*u1 - a/3;
root2 = -u1 - a/3;
return [root1, root2].filter(accept);
}
// one real root, two complex roots
else {
var sd = sqrt(discriminant);
u1 = cuberoot(sd - q2);
v1 = cuberoot(sd + q2);
root1 = u1 - v1 - a/3;
return [root1].filter(accept);
}
}</pre></div>
<p>And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to reduce recomputing things as much as possible" and now we have a way to find all roots for a cubic function and can just move on with using that to find extremities of our curves.</p>
<h3>Quintic and higher order curves: finding numerical solutions</h3>
<p>The problem with this is that as the order of the curve goes up, we can't actually solve those equations the normal
way. We can't take the function, and then work out what the solutions are. Not to mention that even solving a third
order derivative (for a fourth order curve) is already a royal pain in the backside. We need a better solution. We
need numerical approaches.</p>
<p>That's a fancy word for saying "rather than solve the function, treat the problem as a sequence of identical
operations, the performing of which gets us closer and closer to the real answer". As it turns out, there is a
really nice numerical root finding algorithm, called the <a href="http://en.wikipedia.org/wiki/Newton-Raphson">Newton-Raphson</a>
root finding method (yes, after <strong>that</strong> Newton), which we can make use of.</p>
<p>The Newton-Raphson approach consists of picking a value <i>t</i> (any will do), and getting the corresponding
value at that <i>t</i> value. For normal functions, we can treat that value as a height. If the height is zero,
we're done, we have found a root. If it's not, we take the tangent of the curve at that point, and extend
it until it passes the x-axis, which will be at some new point <i>t</i>. We then repeat the procedure with this
new value, and we keep doing this until we find our root.</p>
<p>Mathematically, this means that for some <i>t</i>, at step <i>n=1</i>, we perform the following calculation
until <i>f<sub>y</sub></i>(<i>t</i>) is zero, so that the next <i>t</i> is the same as the one we already have:</p>
<p>\[
t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_n)}
\]</p>
<p>(The wikipedia article has a decent animation for this process, so I'm not adding a sketch for that here)</p>
<p>Now, this works well only if we can pick good starting points, and our curve is continuously differentiable
and doesn't have oscillations. Glossing over the exact meaning of those terms, the curves we're dealing with
conform to those constraints, so as long as we pick good starting points, this will work. So the question is:
which starting points do we pick?</p>
<p>As it turns out, Newton-Raphson is so blindingly fast, so we could get away with just not picking:
we simply run the algorithm from <i>t=0</i> to <i>t=1</i> at small steps (say, 1/200<sup>th</sup>) and
the result will be all the roots we want. Of course, this may pose problems for high order Bézier
curves: 200 steps for a 200<sup>th</sup> order Bézier curve is going to go wrong, but that's okay:
there is no reason, ever, to use Bézier curves of crazy high orders. You might use a fifth order curve
to get the "nicest still remotely workable" approximation of a full circle with a single Bézier curve,
that's pretty much as high as you'll ever need to go.</p>
<h3>In conclusion:</h3>
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics:</p>
<Graphic preset="simple" title="Quadratic Bézier curve extremities" setup={this.setupQuadratic} draw={this.draw}/>
<Graphic preset="simple" title="Cubic Bézier curve extremities" setup={this.setupCubic} draw={this.draw}/>
</section>
);
}
});
module.exports = Extremities;

View File

@@ -77,7 +77,7 @@ var Flattening = React.createClass({
<pre>function flattenCurve(curve, segmentCount):
step = 1/segmentCount;
coordinates = [curve.getXValue(0), curve.getYValue(0)]
for(i=1; i &lt;= segmentCount; i++):
for(i=1; i <= segmentCount; i++):
t = i*step;
coordinates.push[curve.getXValue(t), curve.getYValue(t)]
return coordinates;</pre>
@@ -87,7 +87,7 @@ var Flattening = React.createClass({
<pre>function drawFlattenedCurve(curve, segmentCount):
coordinates = flattenCurve(curve, segmentCount)
coord = coordinates[0], _coords;
for(i=1; i &lt; coordinates.length; i++):
for(i=1; i < coordinates.length; i++):
_coords = coordinates[i]
line(coords, _coords)
coords = _coords</pre>

View File

@@ -16,14 +16,14 @@ module.exports = {
matrixsplit: require("./matrixsplit"),
reordering: require("./reordering"),
derivatives: require("./derivatives")
derivatives: require("./derivatives"),
pointvectors: require("./pointvectors"),
components: require("./components"),
extremities: require("./extremities")
};
/*
pointvectors: require("./pointvectors"),
components: require("./components"),
extremities: require("./extremities"),
boundingbox: require("./boundingbox"),
aligning: require("./aligning"),
tightbounds: require("./tightbounds"),

View File

@@ -179,7 +179,7 @@ var MatrixSplit = React.createClass({
1 & t & t^2
\end{bmatrix}
\cdot
\underset{we\ turn\ this...}{\underbrace{Z \cdot M}}
\underset{we\ turn\ this...}{\underbrace{\kern 2.25em Z \cdot M \kern 2.25em}}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
@@ -192,7 +192,7 @@ var MatrixSplit = React.createClass({
1 & t & t^2
\end{bmatrix}
\cdot
\underset{...into\ this!}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }}
\underset{...into\ this...}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
@@ -206,9 +206,7 @@ var MatrixSplit = React.createClass({
\end{bmatrix}
\cdot
M
\cdot
Q
\cdot
\underset{...to\ get\ \ this!}{\underbrace{ \kern 1.25em \cdot \kern 1.25em Q \kern 1.25em \cdot \kern 1.25em}}
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}

View File

@@ -0,0 +1,145 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var PointVectors = React.createClass({
statics: {
title: "Tangents and normals"
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var i,t,p,tg,n,td=0.25,nd=20;
for(i=0; i<=10; i++) {
t = i/10.0;
p = curve.get(t);
tg = curve.derivative(t);
n = curve.normal(t);
api.setColor("blue");
api.drawLine(p, {x:p.x+tg.x*td, y:p.y+tg.y*td});
api.setColor("red");
api.drawLine(p, {x:p.x+n.x*nd, y:p.y+n.y*nd});
api.setColor("black");
api.drawCircle(p,3);
}
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ PointVectors.title }</SectionHeader>
<p>If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested
in are the tangent vector and normal vector for curve points. These are actually really easy to find. For
moving, and orienting, along a curve we use the tangent, which indicates the direction travel at specific
points, and is literally just the first derivative of our curve:</p>
<p>\[
\left \{ \begin{matrix}
tangent_x(t) = B'_x(t) \\
tangent_y(t) = B'_y(t)
\end{matrix} \right. \]</p>
<p>This gives us the directional vector we want. We can normalize it to give us uniform directional vectors
(having a length of 1.0) at each point, and then do whatever it is we want to do based on those directions:</p>
<p>\[
d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2}
\]</p>
<p>\[
\left \{ \begin{matrix}
\hat{x}(t) = || tangent_x(t) ||
=\frac{tangent_x(t)}{ || tangent(t) || }
= \frac{B'_x(t)}{d} \\
\hat{y}(t) = || tangent_y(t) ||
= \frac{tangent_y(t)}{ || tangent(t) || }
= \frac{B'_y(t)}{d}
\end{matrix} \right. \]</p>
<p>The tangent is very useful for moving along a line, but what if we want to move away from the curve instead,
perpendicular to the curve at some point <i>t</i>? In that case we want the "normal" vector. This vector runs
at a right angle to the direction of the curve, and is typically of length 1.0, so all we have to do is rotate
the normalized directional vector and we're done:</p>
<p>\[
\left \{ \begin{array}{l}
normal_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\
normal_y(t) = \underset{quarter\ circle\ rotation} {\underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }} = \hat{x}(t)
\end{array} \right. \]</p>
<div className="note">
<p>Rotating coordinates is actually very easy, if you know the rule for it. You might find
it explained as "applying a <a href="https://en.wikipedia.org/wiki/Rotation_matrix">rotation matrix</a>",
which is what we'll look at here, too. Essentially, the idea is to take the circles over
which we can rotate, and simply "sliding the coordinates" over those circles by the desired
angle. If we want a quarter circle turn, we take the coordinate, slide it along the cirle
by a quarter turn, and done.</p>
<p>To turn any point <i>(x,y)</i> into a rotated point <i>(x',y')</i> (over 0,0) by
some angle φ, we apply this nicely easy computation:</p>
<p>\[\begin{array}{l}
x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\
y' = x \cdot \sin(\phi) + y \cdot \cos(\phi)
\end{array}\]</p>
<p>Which is the "long" version of the following matrix transformation:</p>
<p>\[
\begin{bmatrix}
x' \\ y'
\end{bmatrix}
=
\begin{bmatrix}
\cos(\phi) & -\sin(\phi) \\
\sin(\phi) & \cos(\phi)
\end{bmatrix}
\begin{bmatrix}
x \\ y
\end{bmatrix}
\]</p>
<p>And that's all we need to rotate any coordinate. Note that for quarter, half
and three quarter turns these functions become even easier, since <i>sin</i> and
<i>cos</i> for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.</p>
<p>But <strong><em>why</em></strong> does this work? Why this matrix multiplication?
<a href="http://en.wikipedia.org/wiki/Rotation_matrix#Decomposition_into_shears">wikipedia</a>
(Technically, Thomas Herter and Klaus Lott) tells us that a rotation matrix can be
treated as a sequence of three (elementary) shear operations. When we combine this into
a single matrix operation (because all matrix multiplications can be collapsed), we get
the matrix that you see above.
<a href="http://datagenetics.com/blog/august32013/index.html">DataGenetics</a> have an
excellent article about this very thing: it's really quite cool, and I strongly recommend
taking a quick break from this primer to read that article.</p>
</div>
<p>The following two graphics show the tangent and normal along a quadratic and cubic curve, with
the direction vector coloured blue, and the normal vector coloured red.</p>
<div className="figure">
<Graphic preset="simple" title="Quadratic Bézier tangents and normals" inline={true} setup={this.setupQuadratic} draw={this.draw}/>
<Graphic preset="simple" title="Cubic Bézier tangents and normals" inline={true} setup={this.setupCubic} draw={this.draw}/>
</div>
</section>
);
}
});
module.exports = PointVectors;

View File

@@ -124,7 +124,7 @@ function drawCurve(points[], t):
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
for(i=0; i<newpoints.length; i++):
if(i==0):
left.add(points[i])
if(i==newpoints.length-1):