mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-29 19:20:39 +02:00
localizing is now pretty nice
This commit is contained in:
44
components/sections/aligning/content.en-GB.md
Normal file
44
components/sections/aligning/content.en-GB.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Aligning curves
|
||||
|
||||
While there are an incredible number of curves we can define by varying the x- and y-coordinates for the control points, not all curves are actually distinct. For instance, if we define a curve, and then rotate it 90 degrees, it's still the same curve, and we'll find its extremities in the same spots, just at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to "axis-align" it.
|
||||
|
||||
Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first point lies on (0,0), which turns our *n* term polynomial functions into *n-1* term functions. The order stays the same, but we have less terms. Then, we can rotate the curves so that the last point always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the function for the y-component to an *n-2* term function. For instance, if we have a cubic curve such as this:
|
||||
|
||||
\[
|
||||
\left \{ \begin{matrix}
|
||||
x = BLUE[120] \cdot (1-t)^3 BLUE[+ 35] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 220] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 220] \cdot t^3 \\
|
||||
y = BLUE[160] \cdot (1-t)^3 BLUE[+ 200] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 260] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 40] \cdot t^3
|
||||
\end{matrix} \right.
|
||||
\]
|
||||
|
||||
Then translating it so that the first coordinate lies on (0,0), moving all *x* coordinates by -120, and all *y* coordinates by -160, gives us:
|
||||
|
||||
\[
|
||||
\left \{ \begin{matrix}
|
||||
x = BLUE[0] \cdot (1-t)^3 BLUE[- 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 100] \cdot t^3 \\
|
||||
y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 120] \cdot t^3
|
||||
\end{matrix} \right.
|
||||
\]
|
||||
|
||||
If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:
|
||||
|
||||
\[
|
||||
\left \{ \begin{matrix}
|
||||
x = BLUE[0] \cdot (1-t)^3 BLUE[+ 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 12] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\
|
||||
y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 140] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 0] \cdot t^3
|
||||
\end{matrix} \right.
|
||||
\]
|
||||
|
||||
If we drop all the zero-terms, this gives us:
|
||||
|
||||
\[
|
||||
\left \{ \begin{array}{l}
|
||||
x = BLUE[85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 13] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\
|
||||
y = BLUE[40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 141] \cdot 3 \cdot (1-t) \cdot t^2
|
||||
\end{array} \right.
|
||||
\]
|
||||
|
||||
We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae:
|
||||
|
||||
<Graphic preset="twopanel" title="Aligning a quadratic curve" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="twopanel" title="Aligning a cubic curve" setup={this.setupCubic} draw={this.draw} />
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "aligning";
|
||||
|
||||
var Aligning = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Aligning curves"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -62,67 +64,8 @@ var Aligning = React.createClass({
|
||||
api.drawCurve(aligned, offset);
|
||||
},
|
||||
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>While there are an incredible number of curves we can define by varying the x- and y-coordinates for
|
||||
the control points, not all curves are actually distinct. For instance, if we define a curve, and then
|
||||
rotate it 90 degrees, it's still the same curve, and we'll find its extremities in the same spots, just
|
||||
at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to
|
||||
"axis-align" it.</p>
|
||||
|
||||
<p>Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first
|
||||
point lies on (0,0), which turns our <i>n</i> term polynomial functions into <i>n-1</i> term functions.
|
||||
The order stays the same, but we have less terms. Then, we can rotate the curves so that the last point
|
||||
always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the function for
|
||||
the y-component to an <i>n-2</i> term function. For instance, if we have a cubic curve such as this:</p>
|
||||
|
||||
<p>\[
|
||||
\left \{ \begin{matrix}
|
||||
x = BLUE[120] \cdot (1-t)^3 BLUE[+ 35] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 220] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 220] \cdot t^3 \\
|
||||
y = BLUE[160] \cdot (1-t)^3 BLUE[+ 200] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 260] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 40] \cdot t^3
|
||||
\end{matrix} \right. \]</p>
|
||||
|
||||
<p>Then translating it so that the first coordinate lies on (0,0), moving all <i>x</i> coordinates
|
||||
by -120, and all <i>y</i> coordinates by -160, gives us:</p>
|
||||
|
||||
<p>\[
|
||||
\left \{ \begin{matrix}
|
||||
x = BLUE[0] \cdot (1-t)^3 BLUE[- 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 100] \cdot t^3 \\
|
||||
y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 120] \cdot t^3
|
||||
\end{matrix} \right. \]</p>
|
||||
|
||||
<p>If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded
|
||||
for illustrative purposes here) become:</p>
|
||||
|
||||
<p>\[
|
||||
\left \{ \begin{matrix}
|
||||
x = BLUE[0] \cdot (1-t)^3 BLUE[+ 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 12] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\
|
||||
y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 140] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 0] \cdot t^3
|
||||
\end{matrix} \right. \]</p>
|
||||
|
||||
<p>If we drop all the zero-terms, this gives us:</p>
|
||||
|
||||
<p>\[
|
||||
\left \{ \begin{array}{l}
|
||||
x = BLUE[85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 13] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\
|
||||
y = BLUE[40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 141] \cdot 3 \cdot (1-t) \cdot t^2
|
||||
\end{array} \right. \]</p>
|
||||
|
||||
<p>We can see that our original curve definition has been simplified considerably. The following graphics
|
||||
illustrate the result of aligning our example curves to the x-axis, with the cubic case using
|
||||
the coordinates that were just used in the example formulae:</p>
|
||||
|
||||
|
||||
<Graphic preset="twopanel" title="Aligning a quadratic curve" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="twopanel" title="Aligning a cubic curve" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
|
||||
</section>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
16
components/sections/boundingbox/content.en-GB.md
Normal file
16
components/sections/boundingbox/content.en-GB.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Bounding boxes
|
||||
|
||||
If we have the extremities, and the start/end points, a simple for loop that tests for min/max values for x and y means we have the four values we need to box in our curve:
|
||||
|
||||
*Computing the bounding box for a Bézier curve*:
|
||||
|
||||
1. Find all *t* value(s) for the curve derivative's x- and y-roots.
|
||||
2. Discard any *t* value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].
|
||||
3. Determine the lowest and highest value when plugging the values *t=0*, *t=1* and each of the found roots into the original functions: the lowest value is the lower bound, and the highest value is the upper bound for the bounding box we want to construct.
|
||||
|
||||
Applying this approach to our previous root finding, we get the following bounding boxes (with all curve extremity points shown on the curve):
|
||||
|
||||
<Graphic preset="simple" title="Quadratic Bézier bounding box" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="simple" title="Cubic Bézier bounding box" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first need to look at how aligning works.
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "boundingbox";
|
||||
|
||||
var BoundingBox = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Bounding boxes"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -26,38 +28,14 @@ var BoundingBox = React.createClass({
|
||||
api.setColor("black");
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
api.setColor("red");
|
||||
curve.extrema().values.forEach(t => {
|
||||
api.drawCircle(curve.get(t), 3);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>If we have the extremities, and the start/end points, a simple for loop that tests for min/max values for
|
||||
x and y means we have the four values we need to box in our curve:</p>
|
||||
|
||||
<p><i>Computing the bounding box for a Bézier curve</i>:</p>
|
||||
|
||||
<ol>
|
||||
<li>Find all <i>t</i> value(s) for the curve derivative's x- and y-roots.</li>
|
||||
<li>Discard any <i>t</i> value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].</li>
|
||||
<li>Determine the lowest and highest value when plugging the values <i>t=0</i>, <i>t=1</i> and each of the found
|
||||
roots into the original functions: the lowest value is the lower bound, and the highest value is the upper
|
||||
bound for the bounding box we want to construct.</li>
|
||||
</ol>
|
||||
|
||||
<p>Applying this approach to our previous root finding, we get the following bounding boxes (with curve
|
||||
extremities coloured the same as in the root finding graphics):</p>
|
||||
|
||||
<Graphic preset="simple" title="Quadratic Bézier bounding box" setup={this.setupQuadratic} draw={this.draw} />
|
||||
|
||||
<Graphic preset="simple" title="Cubic Bézier bounding box" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
|
||||
<p>We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis,
|
||||
but in order to do so we first need to look at how aligning works.</p>
|
||||
</section>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
283
components/sections/canonical/content.en-GB.md
Normal file
283
components/sections/canonical/content.en-GB.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# Canonical form (for cubic curves)
|
||||
|
||||
While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve. As a curvature controlled by more than one control points, it exhibits all kinds of features like loops, cusps, odd colinear features, and up to two inflection points because the curvature can change direction up to three times. Now, knowing what kind of curve we're dealing with means that some algorithms can be run more efficiently than if we have to implement them as generic solvers, so is there a way to determine the curve type without lots of work?
|
||||
|
||||
As it so happens, the answer is yes and the solution we're going to look at was presented by Maureen C. Stone from Xerox PARC and Tony D. deRose from the University of Washington in their joint paper ["A Geometric Characterization of Parametric Cubic curves"](http://graphics.pixar.com/people/derose/publications/CubicClassification/paper.pdf). It was published in 1989, and defines curves as having a "canonical" form (i.e. a form that all curves can be reduced to) from which we can immediately tell which features a curve will have. So how does it work?
|
||||
|
||||
The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:
|
||||
|
||||
<Graphic static={true} preset="simple" title="The canonical curve map" setup={this.setup} draw={this.drawBase} />
|
||||
|
||||
This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0), (0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have certain features. Namely, if the fourth point is...
|
||||
|
||||
1. anywhere on and in the red zone, the curve will be self-intersecting, yielding either a cusp or a loop. Anywhere inside the the red zone, this will be a loop. We won't know <i>where</i> that loop is (in terms of <i>t</i> values), but we are guaranteed that there is one.
|
||||
2. on the left (red) edge, the curve will have a cusp. We again don't know <em>where</em>, just that it
|
||||
has one. This edge is described by the function:
|
||||
|
||||
\[
|
||||
y = \frac{-x^2 + 2x + 3}{4}, \{ x \leq 1 \}
|
||||
\]
|
||||
|
||||
3. on the lower right (pink) edge, the curve will have a loop at t=1, so we know the end coordinate of
|
||||
the curve also lies <em>on</em> the curve. This edge is described by the function:
|
||||
|
||||
\[
|
||||
y = \frac{\sqrt{3(4x - x^2)} - x}{2}, \{ 0 \leq x \leq 1 \}
|
||||
\]
|
||||
|
||||
4. on the top (blue) edge, the curve will have a loop at t=0, so we know the start coordinate of
|
||||
the curve also lies <em>on</em> the curve. This edge is described by the function:
|
||||
|
||||
\[
|
||||
y = \frac{-x^2 + 3x}{3}, \{ x \leq 0 \}
|
||||
\]
|
||||
|
||||
5. inside the green zone, the curve will have a single inflection, switching concave/convex once.
|
||||
6. between the red and green zones, the curve has two inflections, meaning its curvature switches between
|
||||
concave/convex form twice.
|
||||
7. anywhere on the right of the red zone, the curve will have no inflections. It'll just be a well-behaved arch.
|
||||
|
||||
Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.
|
||||
|
||||
<div className="note">
|
||||
|
||||
### Wait, where do those lines come from?
|
||||
|
||||
Without repeating the paper mentioned at the top of this section, the loop-boundaries come from rewriting the curve into canonical form, and then solving the formulae for which constraints must hold for which possible curve properties. In the paper these functions yield formulae for where you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived for the full cubic expression, meaning they apply to t=-∞ to t=∞... For Bézier curves we only care about the "clipped interval" t=0 to t=1, so some of the properties that apply when you look at the curve over an infinite interval simply don't apply to the Bézier curve interval.
|
||||
|
||||
The right bound for the loop region, indicating where the curve switches from "having inflections" to "having a loop", for the general cubic curve, is actually mirrored over x=1, but for Bézier curves this right half doesn't apply, so we don't need to pay attention to it. Similarly, the boundaries for t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general curve does over the interval t=0 to t=1.
|
||||
|
||||
For the full details, head over to the paper and read through sections 3 and 4. If you still remember your high school precalculus, you can probably follow along with this paper, although you might have to read it a few times before all the bits "click".
|
||||
|
||||
</div>
|
||||
|
||||
So now the question becomes: how do we manipulate our curve so that it fits this canonical form, with three fixed points, and one "free" point? Enter linear algerba. Don't worry, I'll be doing all the math for you, as well as show you what the effect is on our curves, but basically we're going to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus approach is very hard to work with, when the equivalent geometrical solution is super obvious.
|
||||
|
||||
The approach is going to start with a curve that doesn't have all-colinear points (so we need to make sure the points don't all fall on a straight line), and then applying four graphics operations that you will probably have heard of: translation (moving all points by some fixed x- and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an operation that turns rectangles into parallelograms).
|
||||
|
||||
Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going to make use of an interesting trick here, by pretending our 2D coordinates are 3D, with the <i>z</i> coordinate simply always being 1. This is an old trick in graphics to overcome the limitations of 2D transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form (ax + by, cx + dy), which means we can't do translation, since that requires we end up with some kind of (x + a, y + b). If we add a bogus <i>z</i> coordinate that is always 1, then we can suddenly add arbitrary values. For example:
|
||||
|
||||
\[
|
||||
\left [ \begin{array}
|
||||
01 & 0 & a \\
|
||||
0 & 1 & b \\
|
||||
0 & 0 & 1
|
||||
\end{array} \right ]
|
||||
\cdot
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x \\
|
||||
y \\
|
||||
z=1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 \cdot x + 0 \cdot y + a \cdot z \\
|
||||
0 \cdot x + 1 \cdot y + b \cdot z \\
|
||||
0 \cdot x + 0 \cdot y + 1 \cdot z
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x + a \cdot 1 \\
|
||||
y + b \cdot 1 \\
|
||||
1 \cdot z
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x + a \\
|
||||
y + b \\
|
||||
z=1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]
|
||||
|
||||
Sweet! <i>z</i> stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:
|
||||
|
||||
\[
|
||||
T_1 =
|
||||
\left [ \begin{array}
|
||||
01 & 0 & -{P_1}_x \\
|
||||
0 & 1 & -{P_1}_y \\
|
||||
0 & 0 & 1
|
||||
\end{array} \right ]
|
||||
\cdot
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x \\
|
||||
y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 \cdot x + 0 \cdot y - {P_1}_x \cdot 1 \\
|
||||
0 \cdot x + 1 \cdot y - {P_1}_y \cdot 1 \\
|
||||
0 \cdot x + 0 \cdot y + 1 \cdot 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x - {P_1}_x \\
|
||||
y - {P_1}_y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]
|
||||
|
||||
Running all our coordinates through this transformation gives a new set of coordinates, let's call those <b>U</b>, where the first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the <i>x=0</i> line, so what we want is a transformation matrix that, when we run it, subtracts <i>x</i> from whatever <i>x</i> we currently have. This is called [shearing](https://en.wikipedia.org/wiki/Shear_matrix), and the typical x-shear matrix and its transformation looks like this:
|
||||
|
||||
\[
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 & S & 0 \\
|
||||
0 & 1 & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\cdot
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x \\
|
||||
y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x + S \cdot y \\
|
||||
y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]
|
||||
|
||||
So we want some shearing value that, when multiplied by <i>y</i>, yields <i>-x</i>, so our x coordinate becomes zero. That value is simpy <i>-x/y</i>, because <i>-x/y * y = -x</i>. Done:
|
||||
|
||||
\[
|
||||
T_2 =
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 & -\frac{ {U_2}_x }{ {U_2}_y } & 0 \\
|
||||
0 & 1 & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]
|
||||
|
||||
Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on (0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to [do some scaling](https://en.wikipedia.org/wiki/Scaling_%28geometry%29) to make sure it ends up at (0,1). Additionally, we want point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3<sub>x</sub>, and y-scaling by point2<sub>y</sub>. This is really easy:
|
||||
|
||||
\[
|
||||
T_3 =
|
||||
\left [
|
||||
\begin{matrix}
|
||||
\frac{1}{ {V_3}_x } & 0 & 0 \\
|
||||
0 & \frac{1}{ {V_2}_y } & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]
|
||||
|
||||
Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared point 2 earlier. In this case, we do the same trick, but with `y/x` rather than `x/y` because we're not x-shearing but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:
|
||||
|
||||
\[
|
||||
T_4 =
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 & 0 & 0 \\
|
||||
\frac{1 - {W_3}_y}{ {W_3}_x } & 1 & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]
|
||||
|
||||
And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:
|
||||
|
||||
\[
|
||||
mapped_4 = \left (
|
||||
\begin{matrix}
|
||||
x = \left (
|
||||
\frac
|
||||
{
|
||||
-x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2}
|
||||
}
|
||||
{
|
||||
-x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2}
|
||||
}
|
||||
\right )
|
||||
\\
|
||||
y = \left (
|
||||
\frac{(-y_1+y_4)}{-y_1+y_2}
|
||||
+
|
||||
\frac
|
||||
{
|
||||
\left ( 1 - \frac{-y_1+y_3}{-y_1+y_2} \right )
|
||||
\left ( -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} \right )
|
||||
}
|
||||
{
|
||||
-x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2}
|
||||
}
|
||||
\right )
|
||||
\end{matrix}
|
||||
\right )
|
||||
\]
|
||||
|
||||
That looks very complex, but notice that every coordinate value is being offset by the initial translation, and a lot of terms in there repeat: it's pretty easy to calculate this fast, since there's so much we can cache and reuse while we compute this mapped coordinate!
|
||||
|
||||
First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?
|
||||
|
||||
\[
|
||||
... = \left (
|
||||
\begin{matrix}
|
||||
x = \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right )
|
||||
\\
|
||||
y =
|
||||
\frac{y_4}{y_2}
|
||||
+
|
||||
\left ( 1 - \frac{y_3}{y_2} \right )
|
||||
\cdot
|
||||
\left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right )
|
||||
\end{matrix}
|
||||
\right )
|
||||
\]
|
||||
|
||||
Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:
|
||||
|
||||
\[
|
||||
... = \left (
|
||||
\begin{matrix}
|
||||
x = (x_4 - x_2 \cdot f_{42}) / ( x_3- x_2 \cdot f_{32} )
|
||||
\\
|
||||
y =
|
||||
f_{42}
|
||||
+
|
||||
\left ( 1 - f_{32} \right )
|
||||
\cdot
|
||||
x
|
||||
\end{matrix}
|
||||
\right ), f_{32} = \frac{y_3}{y_2}, f_{42} = \frac{y_4}{y_2}
|
||||
\]
|
||||
|
||||
That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it look!
|
||||
|
||||
<div className="note">
|
||||
|
||||
### How do you track all that?
|
||||
|
||||
Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply use [Mathematica](http://www.wolfram.com/mathematica). Tracking all this math by hand is insane, and we invented computers, literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the program do the math for me. And real math, too, with symbols, not with numbers. In fact, [here's](http://pomax.github.io/gh-weblog/downloads/canonical-curve.nb) the Mathematica notebook if you want to see how this works for yourself.
|
||||
|
||||
Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's [$295 for home use](http://www.wolfram.com/mathematica-home-edition), but it's **also** [free when you buy a $35 raspberry pi](http://www.wolfram.com/raspberry-pi). Obviously, I bought a raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to *do*, Mathematica can just do it for you. And we don't have to be geniusses to work out what the maths looks like. That's what we have computers for.
|
||||
|
||||
</div>
|
||||
|
||||
So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:
|
||||
|
||||
<Graphic preset="simple" title="A cubic curve mapped to canonical form" setup={this.setup} draw={this.draw} />
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "canonical";
|
||||
|
||||
var Canonical = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Canonical form (for cubic curves)"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -167,329 +169,8 @@ var Canonical = React.createClass({
|
||||
api._map_loaded = true;
|
||||
},
|
||||
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve.
|
||||
As a curvature controlled by more than one control points, it exhibits all kinds of features like loops,
|
||||
cusps, odd colinear features, and up to two inflection points because the curvature can change direction
|
||||
up to three times. Now, knowing what kind of curve we're dealing with means that some algorithms can be
|
||||
run more efficiently than if we have to implement them as generic solvers, so is there a way to determine
|
||||
the curve type without lots of work?</p>
|
||||
|
||||
<p>As it so happens, the answer is yes and the solution we're going to look at was presented by Maureen C. Stone
|
||||
from Xerox PARC and Tony D. deRose from the University of Washington in their joint paper
|
||||
<a href="http://graphics.pixar.com/people/derose/publications/CubicClassification/paper.pdf">"A Geometric
|
||||
Characterization of Parametric Cubic curves"</a>. It was published in 1989, and defines curves as having a
|
||||
"canonical" form (i.e. a form that all curves can be reduced to) from which we can immediately tell which
|
||||
features a curve will have. So how does it work?</p>
|
||||
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply
|
||||
a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the
|
||||
last point then being "somewhere". After applying that transformation, the location of that last point can then
|
||||
tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
|
||||
<Graphic static={true} preset="simple" title="The canonical curve map" setup={this.setup} draw={this.drawBase} />
|
||||
|
||||
<p>This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0),
|
||||
(0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have
|
||||
certain features. Namely, if the fourth point is...</p>
|
||||
|
||||
<ol>
|
||||
<li>anywhere on and in the red zone, the curve will be self-intersecting, yielding either a cusp or a loop.
|
||||
Anywhere inside the the red zone, this will be a loop. We won't know <i>where</i> that loop
|
||||
is (in terms of <i>t</i> values), but we are guaranteed that there is one.</li>
|
||||
<li>on the left (red) edge, the curve will have a cusp. We again don't know <em>where</em>, just that it
|
||||
has one. This edge is described by the function: \[
|
||||
y = \frac{-x^2 + 2x + 3}{4}, \{ x \leq 1 \}
|
||||
\]</li>
|
||||
<li>on the lower right (pink) edge, the curve will have a loop at t=1, so we know the end coordinate of
|
||||
the curve also lies <em>on</em> the curve. This edge is described by the function: \[
|
||||
y = \frac{\sqrt{3(4x - x^2)} - x}{2}, \{ 0 \leq x \leq 1 \}
|
||||
\]</li>
|
||||
<li>on the top (blue) edge, the curve will have a loop at t=0, so we know the start coordinate of
|
||||
the curve also lies <em>on</em> the curve. This edge is described by the function: \[
|
||||
y = \frac{-x^2 + 3x}{3}, \{ x \leq 0 \}
|
||||
\]</li>
|
||||
<li>inside the green zone, the curve will have a single inflection, switching concave/convex once.</li>
|
||||
<li>between the red and green zones, the curve has two inflections, meaning its curvature switches between
|
||||
concave/convex form twice.</li>
|
||||
<li>anywhere on the right of the red zone, the curve will have no inflections. It'll just be a well-behaved arch.</li>
|
||||
</ol>
|
||||
|
||||
<p>Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.</p>
|
||||
|
||||
<div className="note">
|
||||
<h3>Wait, where do those lines come from?</h3>
|
||||
|
||||
<p>Without repeating the paper mentioned at the top of this section, the loop-boundaries come from
|
||||
rewriting the curve into canonical form, and then solving the formulae for which constraints must
|
||||
hold for which possible curve properties. In the paper these functions yield formulae for where
|
||||
you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived
|
||||
for the full cubic expression, meaning they apply to t=-∞ to t=∞... For Bézier curves we only care
|
||||
about the "clipped interval" t=0 to t=1, so some of the properties that apply when you look at
|
||||
the curve over an infinite interval simply don't apply to the Bézier curve interval.</p>
|
||||
|
||||
<p>The right bound for the loop region, indicating where the curve switches from "having inflections"
|
||||
to "having a loop", for the general cubic curve, is actually mirrored over x=1, but for Bézier curves
|
||||
this right half doesn't apply, so we don't need to pay attention to it. Similarly, the boundaries for
|
||||
t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general
|
||||
curve does over the interval t=0 to t=1.</p>
|
||||
|
||||
<p>For the full details, head over to the paper and read through sections 3 and 4. If you still remember
|
||||
your high school precalculus, you can probably follow along with this paper, although you might have
|
||||
to read it a few times before all the bits "click".</p>
|
||||
</div>
|
||||
|
||||
<p>So now the question becomes: how do we manipulate our curve so that it fits this canonical form,
|
||||
with three fixed points, and one "free" point? Enter linear algerba. Don't worry, I'll be doing all
|
||||
the math for you, as well as show you what the effect is on our curves, but basically we're going
|
||||
to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus
|
||||
approach is very hard to work with, when the equivalent geometrical solution is super obvious.</p>
|
||||
|
||||
<p>The approach is going to start with a curve that doesn't have all-colinear points (so we
|
||||
need to make sure the points don't all fall on a straight line), and then applying four graphics
|
||||
operations that you will probably have heard of: translation (moving all points by some fixed x-
|
||||
and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an
|
||||
operation that turns rectangles into parallelograms).</p>
|
||||
|
||||
<p>Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going
|
||||
to make use of an interesting trick here, by pretending our 2D coordinates are 3D, with the <i>z</i>
|
||||
coordinate simply always being 1. This is an old trick in graphics to overcome the limitations of 2D
|
||||
transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form
|
||||
(ax + by, cx + dy), which means we can't do translation, since that requires we end up with some kind
|
||||
of (x + a, y + b). If we add a bogus <i>z</i> coordinate that is always 1, then we can suddenly add
|
||||
arbitrary values. For example:</p>
|
||||
|
||||
<p>\[
|
||||
\left [ \begin{array}
|
||||
01 & 0 & a \\
|
||||
0 & 1 & b \\
|
||||
0 & 0 & 1
|
||||
\end{array} \right ]
|
||||
\cdot
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x \\
|
||||
y \\
|
||||
z=1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 \cdot x + 0 \cdot y + a \cdot z \\
|
||||
0 \cdot x + 1 \cdot y + b \cdot z \\
|
||||
0 \cdot x + 0 \cdot y + 1 \cdot z
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x + a \cdot 1 \\
|
||||
y + b \cdot 1 \\
|
||||
1 \cdot z
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x + a \\
|
||||
y + b \\
|
||||
z=1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>Sweet! <i>z</i> stays 1, so we can effectively ignore it entirely, but we added some plain values
|
||||
to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:</p>
|
||||
|
||||
<p>\[ T_1 =
|
||||
\left [ \begin{array}
|
||||
01 & 0 & -{P_1}_x \\
|
||||
0 & 1 & -{P_1}_y \\
|
||||
0 & 0 & 1
|
||||
\end{array} \right ]
|
||||
\cdot
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x \\
|
||||
y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 \cdot x + 0 \cdot y - {P_1}_x \cdot 1 \\
|
||||
0 \cdot x + 1 \cdot y - {P_1}_y \cdot 1 \\
|
||||
0 \cdot x + 0 \cdot y + 1 \cdot 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x - {P_1}_x \\
|
||||
y - {P_1}_y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>Running all our coordinates through this transformation gives a new set of coordinates, let's call those <b>U</b>, where the first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the <i>x=0</i> line, so what we want is a transformation matrix that, when we run it, subtracts <i>x</i> from whatever <i>x</i> we currently have. This is called <a href="https://en.wikipedia.org/wiki/Shear_matrix">shearing</a>, and the typical x-shear matrix and its transformation looks like this:</p>
|
||||
|
||||
<p>\[
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 & S & 0 \\
|
||||
0 & 1 & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\cdot
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x \\
|
||||
y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
=
|
||||
\left [
|
||||
\begin{matrix}
|
||||
x + S \cdot y \\
|
||||
y \\
|
||||
1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>So we want some shearing value that, when multiplied by <i>y</i>, yields <i>-x</i>, so our x coordinate becomes zero. That value is simpy <i>-x/y</i>, because <i>-x/y * y = -x</i>. Done:</p>
|
||||
|
||||
<p>\[ T_2 =
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 & -\frac{ {U_2}_x }{ {U_2}_y } & 0 \\
|
||||
0 & 1 & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on (0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to [do some scaling](https://en.wikipedia.org/wiki/Scaling_%28geometry%29) to make sure it ends up at (0,1). Additionally, we want point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3<sub>x</sub>, and y-scaling by point2<sub>y</sub>. This is really easy:</p>
|
||||
|
||||
<p>\[ T_3 =
|
||||
\left [
|
||||
\begin{matrix}
|
||||
\frac{1}{ {V_3}_x } & 0 & 0 \\
|
||||
0 & \frac{1}{ {V_2}_y } & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared point 2 earlier. In this case, we do the same trick, but with `y/x` rather than `x/y` because we're not x-shearing but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:</p>
|
||||
|
||||
<p>\[ T_4 =
|
||||
\left [
|
||||
\begin{matrix}
|
||||
1 & 0 & 0 \\
|
||||
\frac{1 - {W_3}_y}{ {W_3}_x } & 1 & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:</p>
|
||||
|
||||
<p>\[
|
||||
mapped_4 = \left (
|
||||
\begin{matrix}
|
||||
x = \left (
|
||||
\frac
|
||||
{
|
||||
-x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2}
|
||||
}
|
||||
{
|
||||
-x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2}
|
||||
}
|
||||
\right )
|
||||
\\
|
||||
y = \left (
|
||||
\frac{(-y_1+y_4)}{-y_1+y_2}
|
||||
+
|
||||
\frac
|
||||
{
|
||||
\left ( 1 - \frac{-y_1+y_3}{-y_1+y_2} \right )
|
||||
\left ( -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} \right )
|
||||
}
|
||||
{
|
||||
-x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2}
|
||||
}
|
||||
\right )
|
||||
\end{matrix}
|
||||
\right )
|
||||
\]</p>
|
||||
|
||||
<p>That looks very complex, but notice that every coordinate value is being offset by the initial translation, and a lot of terms in there repeat: it's pretty easy to calculate this fast, since there's so much we can cache and reuse while we compute this mapped coordinate!</p>
|
||||
|
||||
<p>First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?</p>
|
||||
|
||||
<p>\[
|
||||
... = \left (
|
||||
\begin{matrix}
|
||||
x = \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right )
|
||||
\\
|
||||
y =
|
||||
\frac{y_4}{y_2}
|
||||
+
|
||||
\left ( 1 - \frac{y_3}{y_2} \right )
|
||||
\cdot
|
||||
\left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right )
|
||||
\end{matrix}
|
||||
\right )
|
||||
\]</p>
|
||||
|
||||
<p>Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped
|
||||
y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate
|
||||
it. In fact, let's pull out all those common factors to see just how simple this is:</p>
|
||||
|
||||
<p>\[
|
||||
... = \left (
|
||||
\begin{matrix}
|
||||
x = (x_4 - x_2 \cdot f_{42}) / ( x_3- x_2 \cdot f_{32} )
|
||||
\\
|
||||
y =
|
||||
f_{42}
|
||||
+
|
||||
\left ( 1 - f_{32} \right )
|
||||
\cdot
|
||||
x
|
||||
\end{matrix}
|
||||
\right ), f_{32} = \frac{y_3}{y_2}, f_{42} = \frac{y_4}{y_2}
|
||||
\]</p>
|
||||
|
||||
<p>That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it look!</p>
|
||||
|
||||
<div className="note">
|
||||
<h3>How do you track all that?</h3>
|
||||
|
||||
<p>Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply use <a href="http://www.wolfram.com/mathematica">Mathematica</a>. Tracking all this math by hand is insane, and we invented computers, literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the program do the math for me. And real math, too, with symbols, not with numbers. In fact, <a href="http://pomax.github.io/gh-weblog/downloads/canonical-curve.nb">here's</a> the Mathematica notebook if you want to see how this works for yourself.</p>
|
||||
|
||||
<p>Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's <a href="http://www.wolfram.com/mathematica-home-edition">$295 for home use</a>, but it's <strong>also</strong> <a href="http://www.wolfram.com/raspberry-pi">free when you buy a $35 raspberry pi</a>. Obviously, I bought a raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to <em>do</em>, Mathematica can just do it for you. And we don't have to be geniusses to work out what the maths looks like. That's what we have computers for.</p>
|
||||
</div>
|
||||
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our
|
||||
canonical map, so that we can immediately tell which features our curve must have, based on where the fourth
|
||||
coordinate is located on the map:</p>
|
||||
|
||||
<Graphic preset="simple" title="A cubic curve mapped to canonical form" setup={this.setup} draw={this.draw} />
|
||||
|
||||
</section>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
12
components/sections/components/content.en-GB.md
Normal file
12
components/sections/components/content.en-GB.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Component functions
|
||||
|
||||
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.
|
||||
|
||||
The solution: compute the derivative for each axis separately, and then fit them back together in the same way we do for the original.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
<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}/>
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "components";
|
||||
|
||||
var Components = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Component functions"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -48,36 +50,7 @@ var Components = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<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>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "control";
|
||||
|
||||
var Control = React.createClass({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "decasteljau";
|
||||
|
||||
var deCasteljau = React.createClass({
|
||||
|
149
components/sections/derivatives/content.en-GB.md
Normal file
149
components/sections/derivatives/content.en-GB.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Derivatives
|
||||
|
||||
There's a number of useful things that you can do with Bézier curves based on their derivative, and one of the more amusing observations about Bézier curves is that their derivatives are, in fact, also Bézier curves. In fact, the derivation of a Bézier curve is relatively straight forward, although we do need a bit of math. First, let's look at the derivative rule for Bézier curves, which is:
|
||||
|
||||
\[
|
||||
Bézier'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot Bézier(n-1,t)_i
|
||||
\]
|
||||
|
||||
which we can also write (observing that <i>b</i> in this formula is the same as our <i>w</i> weights, and that <i>n</i> times a summation is the same as a summation where each term is multiplied by <i>n</i>) as:
|
||||
|
||||
\[
|
||||
Bézier'(n,t) = \sum_{i=0}^{n-1} Bézier(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i)
|
||||
\]
|
||||
|
||||
Or, in plain text: the derivative of an n<sup>th</sup> degree Bézier curve is an (n-1)<sup>th</sup> degree Bézier curve, with one fewer term, and new weights w'<sub>0</sub>...w'<sub>n-1</sub> derived from the original weights as n(w<sub>i+1</sub> - w<sub>i</sub>), so for a 3rd degree curve, with four weights, the derivative has three new weights w'<sub>0</sub> = 3(w<sub>1</sub>-w<sub>0</sub>), w'<sub>1</sub> = 3(w<sub>2</sub>-w<sub>1</sub>) and w'<sub>2</sub> = 3(w<sub>3</sub>-w<sub>2</sub>).
|
||||
|
||||
<div className="note">
|
||||
|
||||
### "Slow down, why is that true?"
|
||||
|
||||
Sometimes just being told "this is the derivative" is nice, but you might want to see why this is indeed the case. As such, let's have a look at the proof for this derivative. First off, the weights are independent of the full Bézier function, so the derivative involves only the derivative of the polynomial basis function. So, let's find that:
|
||||
|
||||
\[
|
||||
B_{n,k}(t) \frac{d}{dt} = {n \choose k} t^k (1-t)^{n-k} \frac{d}{dt}
|
||||
\]
|
||||
|
||||
Applying the [product](http://en.wikipedia.org/wiki/Product_rule) and [chain](http://en.wikipedia.org/wiki/Chain_rule) rules gives us:
|
||||
|
||||
\[\begin{array}{l}
|
||||
... &= {n \choose k} \left (
|
||||
k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1
|
||||
\right )
|
||||
\end{array}\]
|
||||
|
||||
Which is hard to work with, so let's expand that properly:
|
||||
|
||||
\[\begin{array}{l}
|
||||
... &= \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k}
|
||||
\end{array}\]
|
||||
|
||||
Now, the trick is to turn this expression into something that has binomial coefficients again, so we want to end up with things that look like "x! over y!(x-y)!". If we can do that in a way that involves terms of <i>n-1</i> and <i>k-1</i>, we'll be on the right track.
|
||||
|
||||
\[\begin{array}{l}
|
||||
... &= \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\
|
||||
|
||||
... &= n \left (
|
||||
\frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k}
|
||||
\right ) \\
|
||||
|
||||
... &= n \left (
|
||||
\frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{(k-1)} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k}
|
||||
\right )
|
||||
\end{array}\]
|
||||
|
||||
And that's the first part done: the two components inside the parentheses are actually regular, lower order Bezier expressions:
|
||||
|
||||
\[\begin{array}{l}
|
||||
... &= n \left (
|
||||
\frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k}
|
||||
\right )
|
||||
\ ,\ with\ x=n-1,\ y=k-1
|
||||
\\
|
||||
|
||||
|
||||
... &= n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right )
|
||||
\end{array}\]
|
||||
|
||||
Now to apply this to our weighted Bezier curves. We'll write out the plain curve formula that we saw earlier, and then work our way through to its derivative:
|
||||
|
||||
\[\begin{array}{l}
|
||||
Bézier_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + ... \\
|
||||
Bézier_{n,k}(t) \frac{d}{dt} &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + \\
|
||||
& & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + \\
|
||||
& & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + \\
|
||||
& & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + \\
|
||||
& & ...
|
||||
\end{array}\]
|
||||
|
||||
If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for <i>k</i> we see the following:
|
||||
|
||||
\[\begin{array}{l}
|
||||
n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\
|
||||
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\
|
||||
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\
|
||||
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\
|
||||
... &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\
|
||||
... & & &
|
||||
\end{array}\]
|
||||
|
||||
Two of these terms fall way: the first term falls away because there is no -1<sup>st</sup> term in a summation. As such, it always contributes "nothing", so we can safely completely ignore it for the purpose of finding the derivative function. The other term is the very last term in this expansion: one involving <i>B<sub>n-1,n</sub></i>. This term would have a binomial coefficient of [<i>i</i> choose <i>i+1</i>], which is a non-existent binomial coefficient. Again, this term would contribute "nothing", so we can ignore it, too. This means we're left with:
|
||||
|
||||
\[\begin{array}{l}
|
||||
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\
|
||||
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& \ n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\
|
||||
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\
|
||||
...
|
||||
\end{array}\]
|
||||
|
||||
And that's just a summation of lower order curves:
|
||||
|
||||
\[
|
||||
Bézier_{n,k}(t) \frac{d}{dt} = n \cdot B_{(n-1),BLUE[0]}(t) \cdot (w_1 - w_0)
|
||||
+ n \cdot B_{(n-1),RED[1]}(t) \cdot (w_2 - w_1)
|
||||
+ n \cdot B_{(n-1),MAGENTA[2]}(t) \cdot (w_3 - w_2)
|
||||
\ + \ ...
|
||||
\]
|
||||
|
||||
We can rewrite this as a normal summation, and we're done:
|
||||
|
||||
\[
|
||||
Bézier_{n,k}(t) \frac{d}{dt} = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k)
|
||||
= \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underset{derivative\ weights}
|
||||
{\underbrace{n \cdot (w_{k+1} - w_k)}}
|
||||
\]
|
||||
|
||||
</div>
|
||||
|
||||
Let's rewrite that in a form similar to our original formula, so we can see the difference. We will first list our original formula for Bézier curves, and then the derivative:
|
||||
|
||||
\[
|
||||
Bézier(n,t) = \sum_{i=0}^{n}
|
||||
\underset{binomial\ term}{\underbrace{\binom{n}{i}}}
|
||||
\cdot\
|
||||
\underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
|
||||
\cdot\
|
||||
\underset{weight}{\underbrace{w_i}}
|
||||
\]
|
||||
|
||||
\[
|
||||
Bézier'(n,t) = \sum_{i=0}^{k}
|
||||
\underset{binomial\ term}{\underbrace{\binom{k}{i}}}
|
||||
\cdot\
|
||||
\underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
|
||||
\cdot\
|
||||
\underset{derivative\ weight}{\underbrace{n \cdot (w_{i+1} - w_i)}}
|
||||
{\ , \ with \ k=n-1}
|
||||
\]
|
||||
|
||||
|
||||
What are the differences? In terms of the actual Bézier curve, virtually nothing! We lowered the order (rather than <i>n</i>, it's now <i>n-1</i>), but it's still the same Bézier function. The only real difference is in how the weights change when we derive the curve's function. If we have four points A, B, C, and D, then the derivative will have three points, the second derivative two, and the third derivative one:
|
||||
|
||||
\[ \begin{array}{l}
|
||||
B(n,t), & & w = \{A,B,C,D\} \\
|
||||
B'(n,t), & n = 3, & w' = \{A',B',C'\} &= \{3 \cdot (B-A), {\ } 3 \cdot (C-B), {\ } 3 \cdot (D-C)\} \\
|
||||
B''(n,t), & n = 2, & w'' = \{A'',B''\} &= \{2 \cdot (B'-A'), {\ } 2 \cdot (C'-B')\} \\
|
||||
B'''(n,t), & n = 1, & w''' = \{A'''\} &= \{1 \cdot (B''-A'')\}
|
||||
\end{array} \]
|
||||
|
||||
We can keep performing this trick for as long as we have more than one weight. Once we have one weight left, the next step will see <i>k = 0</i>, and the result of our "Bézier function" summation is zero, because we're not adding anything at all. As such, a quadratic curve has no second derivative, a cubic curve has no third derivative, and generalized: an <i>n<sup>th</sup></i> order curve has <i>n-1</i> (meaningful) derivatives, with any further derivative being zero.
|
@@ -1,200 +1,18 @@
|
||||
var React = require("react");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "derivatives";
|
||||
|
||||
var Derivatives = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Derivatives"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>There's a number of useful things that you can do with Bézier curves based on their derivative,
|
||||
and one of the more amusing observations about Bézier curves is that their derivatives are, in fact,
|
||||
also Bézier curves. In fact, the derivation of a Bézier curve is relatively straight forward, although
|
||||
we do need a bit of math. First, let's look at the derivative rule for Bézier curves, which is:</p>
|
||||
|
||||
<p>\[
|
||||
Bézier'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot Bézier(n-1,t)_i
|
||||
\]</p>
|
||||
|
||||
<p>which we can also write (observing that <i>b</i> in this formula is the same as our <i>w</i> weights,
|
||||
and that <i>n</i> times a summation is the same as a summation where each term is multiplied by <i>n</i>)
|
||||
as:</p>
|
||||
|
||||
<p>\[
|
||||
Bézier'(n,t) = \sum_{i=0}^{n-1} Bézier(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i)
|
||||
\]</p>
|
||||
|
||||
<p>Or, in plain text: the derivative of an n<sup>th</sup> degree Bézier curve is an (n-1)<sup>th</sup>
|
||||
degree Bézier curve, with one fewer term, and new weights w'<sub>0</sub>...w'<sub>n-1</sub> derived
|
||||
from the original weights as n(w<sub>i+1</sub> - w<sub>i</sub>), so for a 3rd degree curve, with four weights,
|
||||
the derivative has three new weights w'<sub>0</sub> = 3(w<sub>1</sub>-w<sub>0</sub>),
|
||||
w'<sub>1</sub> = 3(w<sub>2</sub>-w<sub>1</sub>) and w'<sub>2</sub> = 3(w<sub>3</sub>-w<sub>2</sub>).</p>
|
||||
|
||||
<div className="note">
|
||||
<h3>"Slow down, why is that true?"</h3>
|
||||
|
||||
<p>Sometimes just being told "this is the derivative" is nice, but you might want to see why
|
||||
this is indeed the case. As such, let's have a look at the proof for this derivative. First off,
|
||||
the weights are independent of the full Bézier function, so the derivative involves only the
|
||||
derivative of the polynomial basis function. So, let's find that:</p>
|
||||
|
||||
<p>\[
|
||||
B_{n,k}(t) \frac{d}{dt} = {n \choose k} t^k (1-t)^{n-k} \frac{d}{dt}
|
||||
\]</p>
|
||||
|
||||
<p>Applying the <a href="http://en.wikipedia.org/wiki/Product_rule">product</a> and
|
||||
<a href="http://en.wikipedia.org/wiki/Chain_rule">chain</a> rules gives us:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
... &= {n \choose k} \left (
|
||||
k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1
|
||||
\right )
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>Which is hard to work with, so let's expand that properly:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
... &= \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k}
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>Now, the trick is to turn this expression into something that has binomial
|
||||
coefficients again, so we want to end up with things that look like "x! over y!(x-y)!".
|
||||
If we can do that in a way that involves terms of <i>n-1</i> and <i>k-1</i>, we'll
|
||||
be on the right track.</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
... &= \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\
|
||||
|
||||
... &= n \left (
|
||||
\frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k}
|
||||
\right ) \\
|
||||
|
||||
... &= n \left (
|
||||
\frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{(k-1)} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k}
|
||||
\right )
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>And that's the first part done: the two components inside the parentheses are actually
|
||||
regular, lower order Bezier expressions:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
... &= n \left (
|
||||
\frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k}
|
||||
\right )
|
||||
\ ,\ with\ x=n-1,\ y=k-1
|
||||
\\
|
||||
|
||||
|
||||
... &= n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right )
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>Now to apply this to our weighted Bezier curves. We'll write out the plain curve formula that
|
||||
we saw earlier, and then work our way through to its derivative:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
Bézier_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + ... \\
|
||||
Bézier_{n,k}(t) \frac{d}{dt} &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + \\
|
||||
& & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + \\
|
||||
& & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + \\
|
||||
& & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + \\
|
||||
& & ...
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for <i>k</i>
|
||||
we see the following:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\
|
||||
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\
|
||||
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\
|
||||
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\
|
||||
... &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\
|
||||
... & & &
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>Two of these terms fall way: the first term falls away because there is no
|
||||
-1<sup>st</sup> term in a summation. As such, it always contributes "nothing", so
|
||||
we can safely completely ignore it for the purpose of finding the derivative function.
|
||||
The other term is the very last term in this expansion: one involving <i>B<sub>n-1,n</sub></i>.
|
||||
This term would have a binomial coefficient of [<i>i</i> choose <i>i+1</i>], which is
|
||||
a non-existent binomial coefficient. Again, this term would contribute "nothing", so we
|
||||
can ignore it, too. This means we're left with:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\
|
||||
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& \ n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\
|
||||
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\
|
||||
...
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>And that's just a summation of lower order curves:</p>
|
||||
|
||||
<p>\[
|
||||
Bézier_{n,k}(t) \frac{d}{dt} = n \cdot B_{(n-1),BLUE[0]}(t) \cdot (w_1 - w_0)
|
||||
+ n \cdot B_{(n-1),RED[1]}(t) \cdot (w_2 - w_1)
|
||||
+ n \cdot B_{(n-1),MAGENTA[2]}(t) \cdot (w_3 - w_2)
|
||||
\ + \ ...
|
||||
\]</p>
|
||||
|
||||
<p>We can rewrite this as a normal summation, and we're done:</p>
|
||||
<p>\[
|
||||
Bézier_{n,k}(t) \frac{d}{dt} = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k)
|
||||
= \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underset{derivative\ weights}
|
||||
{\underbrace{n \cdot (w_{k+1} - w_k)}}
|
||||
\]</p>
|
||||
</div>
|
||||
|
||||
<p>Let's rewrite that in a form similar to our original formula, so we can see the difference. We will
|
||||
first list our original formula for Bézier curves, and then the derivative:</p>
|
||||
|
||||
<p>\[
|
||||
Bézier(n,t) = \sum_{i=0}^{n}
|
||||
\underset{binomial\ term}{\underbrace{\binom{n}{i}}}
|
||||
\cdot\
|
||||
\underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
|
||||
\cdot\
|
||||
\underset{weight}{\underbrace{w_i}}
|
||||
\]</p>
|
||||
|
||||
<p>\[
|
||||
Bézier'(n,t) = \sum_{i=0}^{k}
|
||||
\underset{binomial\ term}{\underbrace{\binom{k}{i}}}
|
||||
\cdot\
|
||||
\underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
|
||||
\cdot\
|
||||
\underset{derivative\ weight}{\underbrace{n \cdot (w_{i+1} - w_i)}}
|
||||
{\ , \ with \ k=n-1}
|
||||
\]</p>
|
||||
|
||||
|
||||
<p>What are the differences? In terms of the actual Bézier curve, virtually nothing!
|
||||
We lowered the order (rather than <i>n</i>, it's now <i>n-1</i>), but it's still the
|
||||
same Bézier function. The only real difference is in how the weights change when we
|
||||
derive the curve's function. If we have four points A, B, C, and D, then the derivative
|
||||
will have three points, the second derivative two, and the third derivative one:</p>
|
||||
|
||||
<p>\[ \begin{array}{l}
|
||||
B(n,t), & & w = \{A,B,C,D\} \\
|
||||
B'(n,t), & n = 3, & w' = \{A',B',C'\} &= \{3 \cdot (B-A), {\ } 3 \cdot (C-B), {\ } 3 \cdot (D-C)\} \\
|
||||
B''(n,t), & n = 2, & w'' = \{A'',B''\} &= \{2 \cdot (B'-A'), {\ } 2 \cdot (C'-B')\} \\
|
||||
B'''(n,t), & n = 1, & w''' = \{A'''\} &= \{1 \cdot (B''-A'')\}
|
||||
\end{array} \]</p>
|
||||
|
||||
<p>We can keep performing this trick for as long as we have more than one weight. Once
|
||||
we have one weight left, the next step will see <i>k = 0</i>, and the result of our
|
||||
"Bézier function" summation is zero, because we're not adding anything at all. As such,
|
||||
a quadratic curve has no second derivative, a cubic curve has no third derivative, and
|
||||
generalized: an <i>n<sup>th</sup></i> order curve has <i>n-1</i> (meaningful) derivatives,
|
||||
with any further derivative being zero.</p>
|
||||
|
||||
</section>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -2,7 +2,7 @@ var React = require("react");
|
||||
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "explanation";
|
||||
|
||||
var Explanation = React.createClass({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "extended";
|
||||
|
||||
var Explanation = React.createClass({
|
||||
|
173
components/sections/extremities/content.en-GB.md
Normal file
173
components/sections/extremities/content.en-GB.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Finding extremities: root finding
|
||||
|
||||
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:
|
||||
|
||||
### Quadratic curves: linear derivatives.
|
||||
|
||||
Finding the solution for "where is this line 0" should be trivial:
|
||||
|
||||
\[
|
||||
\begin{align}
|
||||
l(x) = ax + b &= 0,\\
|
||||
ax + b &= 0,\\
|
||||
ax &= -b \\
|
||||
x &= \frac{-b}{a}
|
||||
\end{align}
|
||||
\]
|
||||
|
||||
Done. And quadratic curves have no meaningful second derivative, so we're *really* done.
|
||||
|
||||
### Cubic curves: the quadratic formula.
|
||||
|
||||
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 [Quadratic formula](https://en.wikipedia.org/wiki/Quadratic_formula). If you've seen it before, you'll remember it, and if you haven't, it looks like this:
|
||||
|
||||
\[
|
||||
Given\ f(t) = at^2 + bt + c,\ f(t)=0\ when\ t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
|
||||
\]
|
||||
|
||||
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?
|
||||
|
||||
First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the [derivatives section](#derivatives):
|
||||
|
||||
\[
|
||||
\begin{align}
|
||||
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)
|
||||
\end{align}
|
||||
\]
|
||||
|
||||
And then, using these *v* values, we can find out what our *a*, *b*, and *c* should be:
|
||||
|
||||
\[
|
||||
\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}
|
||||
\]
|
||||
|
||||
This gives us thee coefficients *a*, *b*, and *c* that are expressed in terms of *v* values, where the *v* values are just convenient expressions of our original *p* values, so we can do some trivial substitution to get:
|
||||
|
||||
\[
|
||||
\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}
|
||||
\]
|
||||
|
||||
Easy peasy. We can now almost trivially find the roots by plugging those values into the quadratic formula. 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.
|
||||
|
||||
### Quartic curves: Cardano's algorithm.
|
||||
|
||||
Quartic—fourth degree—curves 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, [Gerolamo Cardano](https://en.wikipedia.org/wiki/Gerolamo_Cardano) 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:
|
||||
|
||||
\[
|
||||
very\ hard:\ solve\ at^3 + bt^2 + ct + d = 0\\
|
||||
easier:\ solve\ t^3 + pt + q = 0
|
||||
\]
|
||||
|
||||
This is easier because for the "easier formula" we can use [regular calculus](http://www.wolframalpha.com/input/?i=t^3+%2B+pt+%2B+q) 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 *t* is a plain real number from 0 to 1).
|
||||
|
||||
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 [Ken J. Ward's page](http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm) 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.
|
||||
|
||||
<div className="howtocode">
|
||||
|
||||
### Implementing Cardano's algorithm for finding all real roots
|
||||
|
||||
The "real roots" part is fairly important, because while you cannot take a square, cube, etc. root of a negative number in the "real" number space (denoted with ℝ), this is perfectly fine in the ["complex" number](https://en.wikipedia.org/wiki/Complex_number) space (denoted with ℂ). And, as it so happens, Cardano is also attributed as the first mathematician in history to have made use of complex numbers in his calculations. For this very algorithm!
|
||||
|
||||
```
|
||||
// A helper function to filter for values in the [0,1] interval:
|
||||
function accept(t) {
|
||||
return 0<=t && t <=1;
|
||||
}
|
||||
|
||||
// A real-cuberoots-only function:
|
||||
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:
|
||||
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
|
||||
var sd = sqrt(discriminant);
|
||||
u1 = cuberoot(sd - q2);
|
||||
v1 = cuberoot(sd + q2);
|
||||
root1 = u1 - v1 - a/3;
|
||||
return [root1].filter(accept);
|
||||
}
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
### Quintic and higher order curves: finding numerical solutions
|
||||
|
||||
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.
|
||||
|
||||
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 [Newton-Raphson](http://en.wikipedia.org/wiki/Newton-Raphson) root finding method (yes, after *[that](https://en.wikipedia.org/wiki/Isaac_Newton)* Newton), which we can make use of.
|
||||
|
||||
The Newton-Raphson approach consists of picking a value *t* (any will do), and getting the corresponding value at that *t* 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 *t*. We then repeat the procedure with this new value, and we keep doing this until we find our root.
|
||||
|
||||
Mathematically, this means that for some *t*, at step *n=1*, we perform the following calculation until *f<sub>y</sub>*(*t*) is zero, so that the next *t* is the same as the one we already have:
|
||||
|
||||
\[
|
||||
t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_n)}
|
||||
\]
|
||||
|
||||
(The wikipedia article has a decent animation for this process, so I'm not adding a sketch for that here unless there are requests for it)
|
||||
|
||||
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?
|
||||
|
||||
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 *t=0* to *t=1* 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.
|
||||
|
||||
### In conclusion:
|
||||
|
||||
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:
|
||||
|
||||
<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}/>
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "extremities";
|
||||
|
||||
var Extremities = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Finding extremities: root finding"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -62,208 +64,7 @@ var Extremities = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<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>Quartic—fourth degree—curves 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="howtocode">
|
||||
<h3>Implementing Cardano's algorithm for finding all real roots</h3>
|
||||
|
||||
<p>The "real roots" part is fairly important, because while you cannot take a square, cube, etc. root of a
|
||||
negative number in the "real" number space (denoted with ℝ), this is perfectly fine in the <a
|
||||
href="https://en.wikipedia.org/wiki/Complex_number">"complex" number</a> space (denoted with ℂ).
|
||||
And, as it so happens, Cardano is also attributed as the first mathematician in history to have
|
||||
made use of complex numbers in his calculations. For this very algorithm!</p>
|
||||
|
||||
<pre>// A helper function to filter for values in the [0,1] interval:
|
||||
function accept(t) {
|
||||
return 0<=t && t <=1;
|
||||
}
|
||||
|
||||
// A real-cuberoots-only function:
|
||||
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 <em><a href="https://en.wikipedia.org/wiki/Isaac_Newton">that</a></em> 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>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "flattening";
|
||||
|
||||
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
|
||||
|
96
components/sections/inflections/content.en-GB.md
Normal file
96
components/sections/inflections/content.en-GB.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Curve inflections
|
||||
|
||||
Now that we know how to align a curve, there's one more thing we can calculate: inflection points. Imagine we have a variable size circle that we can slide up against our curve. We place it against the curve and adjust its radius so that where it touches the curve, the curvatures of the curve and the circle are the same, and then we start to slide the circle along the curve - for quadratic curves, we can always do this without the circle behaving oddly: we might have to change the radius of the circle as we slide it along, but it'll always sit against the same side of the curve.
|
||||
|
||||
But what happens with cubic curves? Imagine we have an S curve and we place our circle at the start of the curve, and start sliding it along. For a while we can simply adjust the radius and things will be fine, but once we get to the midpoint of that S, something odd happens: the circle "flips" from one side of the curve to the other side, in order for the curvatures to keep matching. This is called an inflection, and we can find out where those happen relatively easily.
|
||||
|
||||
What we need to do is solve a simple equation:
|
||||
|
||||
\[
|
||||
C(t) = 0
|
||||
\]
|
||||
|
||||
What we're saying here is that given the curvature function *C(t)*, we want to know for which values of *t* this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does *C(t)* look like? Actually something that seems not too hard:
|
||||
|
||||
\[
|
||||
C(t) = Bézier_x\prime(t) \cdot Bézier_y{\prime\prime}(t) - Bézier_y\prime(t) \cdot Bézier_x{\prime\prime}(t)
|
||||
\]
|
||||
|
||||
So the function *C(t)* is wholly defined by the first and second derivative functions for the parametric dimensions of our curve. And as already shown, derivatives of Bézier curves are just simpler Bézier curves, with very easy to compute new coefficients, so this should be pretty easy.
|
||||
|
||||
However as we've seen in the section on aligning, aligning lets us simplify things *a lot*, by completely removing the contributions of the first coordinate from most mathematical evaluations, and removing the last *y* coordinate as well by virtue of the last point lying on the x-axis. So, while we can evaluate *C(t) = 0* for our curve, it'll be much easier to first axis-align the curve and *then* evalutating the curvature function.
|
||||
|
||||
<div className="note">
|
||||
|
||||
### Let's derive the full formula anyway
|
||||
|
||||
Of course, before we do our aligned check, let's see what happens if we compute the curvature function without axis-aligning. We start with the first and second derivatives, given our basis functions:
|
||||
|
||||
\[
|
||||
\begin{align*}
|
||||
& Bézier(t) = x_1(1-t)^3 + 3x_2(1-t)^2t + 3x_3(1-t)t^2 + x_4t^3 \\
|
||||
& Bézier^\prime(t) = a(1-t)^2 + 2b(1-t)^t + ct^2\ \left\{ a=3(x_2-x_1),b=3(x_3-x_2),c=3(x_4-x_3) \right\} \\
|
||||
& Bézier^{\prime\prime}(t) = u(1-t) + vt\ \left\{ u=2(b-a),v=2(c-b) \right\}\
|
||||
\end{align*}
|
||||
\]
|
||||
|
||||
And of course the same functions for *y*:
|
||||
|
||||
\[
|
||||
\begin{align*}
|
||||
& Bézier(t) = y_1(1-t)^3 + 3y_2(1-t)^2t + 3y_3(1-t)t^2 + y_4t^3 \\
|
||||
& Bézier^\prime(t) = d(1-t)^2 + 2e(1-t)^t + ft^2\\
|
||||
& Bézier^{\prime\prime}(t) = w(1-t) + zt
|
||||
\end{align*}
|
||||
\]
|
||||
|
||||
Asking a computer to now compose the *C(t)* function for us (and to expand it to a readable form of simple terms) gives us this rather overly complicated set of arithmetic expressions:
|
||||
|
||||
\[
|
||||
\begin{array}
|
||||
-18 t^2 x_2 y_1+36 t^2 x_3 y_1-18 t^2 x_4 y_1+18 t^2 x_1 y_2-54 t^2 x_3 y_2 \\
|
||||
+36 t^2 x_4 y_2-36 t^2 x_1 y_3+54 t^2 x_2 y_3-18 t^2 x_4 y_3+18 t^2 x_1 y_4 \\
|
||||
-36 t^2 x_2 y_4+18 t^2 x_3 y_4+36 t x_2 y_1-54 t x_3 y_1+18 t x_4 y_1-36 t x_1 y_2 \\
|
||||
+54 t x_3 y_2-18 t x_4 y_2+54 t x_1 y_3-54 t x_2 y_3-18 t x_1 y_4+18 t x_2 y_4 \\
|
||||
-18 x_2 y_1+18 x_3 y_1+18 x_1 y_2-18 x_3 y_2-18 x_1 y_3+18 x_2 y_3
|
||||
\end{array}
|
||||
\]
|
||||
|
||||
That is... unwieldy. So, we note that there are a lot of terms that involve multiplications involving x1, y1, and y4, which would all disappear if we axis-align our curve, which is why aligning is a great idea.
|
||||
|
||||
</div>
|
||||
|
||||
Aligning our curve so that three of the eight coefficients become zero, we end up with the following simple term function for *C(t)*:
|
||||
|
||||
\[
|
||||
18 \left ( (3 x_3 y_2+2 x_4 y_2+3 x_2 y_3-x_4 y_3)t^2 + (3 x_3 y_2-x_4 y_2-3 x_2 y_3)t + (x_2 y_3-x_3 y_2) \right )
|
||||
\]
|
||||
|
||||
That's a lot easier to work with: we see a fair number of terms that we can compute and then cache, giving us the following simplification:
|
||||
|
||||
\[
|
||||
\left.\begin{matrix}
|
||||
a = x_3 \cdot y_2 \\
|
||||
b = x_4 \cdot y_2 \\
|
||||
c = x_2 \cdot y_3 \\
|
||||
d = x_4 \cdot y_3
|
||||
\end{matrix}\right\}
|
||||
\ C(t) = 18 \cdot \left ( (-3a + 2b + 3c - d)t^2 + (3a - b - 3c)t + (c - a) \right )
|
||||
\]
|
||||
|
||||
This is a plain quadratic curve, and we know how to solve *C(t) = 0*; we use the quadratic formula:
|
||||
|
||||
\[
|
||||
\left.\begin{matrix}
|
||||
x =& 18(-3a + 2b + 3c - d) \\
|
||||
y =& 18(3a - b - 3c) \\
|
||||
z =& 18(c - a)
|
||||
\end{matrix}\right\}
|
||||
\ C(t) = 0 \ \Rightarrow\ t = \frac{-y \pm \sqrt{y^2 - 4 x z}}{2x}
|
||||
\]
|
||||
|
||||
We can easily compute this value *if* the descriminator isn't a negative number (because we only want real roots, not complex roots), and *if* *x* is not zero, because divisions by zero are rather useless.
|
||||
|
||||
Taking that into account, we compute *t*, we disregard any *t* value that isn't in the Bézier interval [0,1], and we now know at which *t* value(s) our curve will inflect.
|
||||
|
||||
<Graphic title="Finding cubic Bézier curve inflections" setup={this.setupCubic} draw={this.draw}/>
|
@@ -1,16 +1,18 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "inflections";
|
||||
|
||||
var ABC = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Curve inflections"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
setupCubic: function(api) {
|
||||
var curve = api.getDefaultCubic();
|
||||
var curve = new api.Bezier(135,25, 25, 135, 215,75, 215,240);
|
||||
api.setCurve(curve);
|
||||
},
|
||||
|
||||
@@ -19,134 +21,14 @@ var ABC = React.createClass({
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
|
||||
api.setColor("red");
|
||||
curve.inflections().forEach(function(t) {
|
||||
api.drawCircle(curve.get(t), 5);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Now that we know how to align a curve, there's one more thing we can calculate: inflection points.
|
||||
Imagine we have a variable size circle that we can slide up against our curve. We place it against
|
||||
the curve and adjust its radius so that where it touches the curve, the curvatures of the curve and
|
||||
the circle are the same, and then we start to slide the circle along the curve - for quadratic
|
||||
curves, we can always do this without the circle behaviour oddly: we might have to change the
|
||||
radius of the circle as we slide it alone, but it'll always sit against the same side of the curve.</p>
|
||||
|
||||
<p>But what happens with cubic curves? Imagine we have an S curve and we place our circle at the
|
||||
start of the curve, and start sliding it along. For a while we can simply adjust the radius and
|
||||
things will be fine, but once we get to the midpoint of that S, something odd happens: the circle
|
||||
"flips" from one side of the curve to the other side, in order for the curvatures to keep matching.
|
||||
This is called an inflection, and we can find out where those happen relatively easily.</p>
|
||||
|
||||
<p>What we need to do is solve a simple equation:</p>
|
||||
|
||||
<p>\[
|
||||
C(t) = 0
|
||||
\]</p>
|
||||
|
||||
<p>What we're saying here is that given the curvature function <i>C(t)</i>, we want to know for which
|
||||
values of <i>t</i> this function is zero, meaning there is no "curvature", which will be exactly
|
||||
at the point between our circle being on one side of the curve, and our circle being on the other
|
||||
side of the curve. So what does <i>C(t)</i> look like? Actually something that seems not too hard:</p>
|
||||
|
||||
<p>\[
|
||||
C(t) = Bézier_x\prime(t) \cdot Bézier_y{\prime\prime}(t) - Bézier_y\prime(t) \cdot Bézier_x{\prime\prime}(t)
|
||||
\]</p>
|
||||
|
||||
<p>So the function <i>C(t)</i> is wholly defined by the first and second derivative functions for the
|
||||
parametric dimensions of our curve. And as already shown, derivatives of Bézier curves are just
|
||||
simpler Bézier curves, with very easy to compute new coefficients, so this should be pretty easy.</p>
|
||||
|
||||
<p>However as we've seen in the section on aligning, aligning lets us simplify things <em>a lot</em>,
|
||||
by completely removing the contributions of the first coordinate from most mathematical evaluations,
|
||||
and removing the last <i>y</i> coordinate as well by virtue of the last point lying on the x-axis. So,
|
||||
while we can evaluate <i>C(t) = 0</i> for our curve, it'll be much easier to first axis-align the curve
|
||||
and <em>then</em> evalutating the curvature function.</p>
|
||||
|
||||
<div className="note">
|
||||
<h3>Let's derive the full formula anyway</h3>
|
||||
|
||||
<p>Of course, before we do our aligned check, let's see what happens if we compute the curvature
|
||||
function without axis-aligning. We start with the first and second derivatives, given our basis
|
||||
functions:</p>
|
||||
|
||||
<p>\[\begin{align*}
|
||||
& Bézier(t) = x_1(1-t)^3 + 3x_2(1-t)^2t + 3x_3(1-t)t^2 + x_4t^3 \\
|
||||
& Bézier^\prime(t) = a(1-t)^2 + 2b(1-t)^t + ct^2\ \left\{ a=3(x_2-x_1),b=3(x_3-x_2),c=3(x_4-x_3) \right\} \\
|
||||
& Bézier^{\prime\prime}(t) = u(1-t) + vt\ \left\{ u=2(b-a),v=2(c-b) \right\}\
|
||||
\end{align*}\]</p>
|
||||
|
||||
<p>And of course the same functions for <i>y</i>:</p>
|
||||
|
||||
<p>\[\begin{align*}
|
||||
& Bézier(t) = y_1(1-t)^3 + 3y_2(1-t)^2t + 3y_3(1-t)t^2 + y_4t^3 \\
|
||||
& Bézier^\prime(t) = d(1-t)^2 + 2e(1-t)^t + ft^2\\
|
||||
& Bézier^{\prime\prime}(t) = w(1-t) + zt
|
||||
\end{align*}\]</p>
|
||||
|
||||
<p>Asking a computer to now compose the <i>C(t)</i> function for us (and to expand it to
|
||||
a readable form of simple terms) gives us this rather overly complicated set of arithmetic
|
||||
expressions:</p>
|
||||
|
||||
<p>\[\begin{array}
|
||||
-18 t^2 x_2 y_1+36 t^2 x_3 y_1-18 t^2 x_4 y_1+18 t^2 x_1 y_2-54 t^2 x_3 y_2 \\
|
||||
+36 t^2 x_4 y_2-36 t^2 x_1 y_3+54 t^2 x_2 y_3-18 t^2 x_4 y_3+18 t^2 x_1 y_4 \\
|
||||
-36 t^2 x_2 y_4+18 t^2 x_3 y_4+36 t x_2 y_1-54 t x_3 y_1+18 t x_4 y_1-36 t x_1 y_2 \\
|
||||
+54 t x_3 y_2-18 t x_4 y_2+54 t x_1 y_3-54 t x_2 y_3-18 t x_1 y_4+18 t x_2 y_4 \\
|
||||
-18 x_2 y_1+18 x_3 y_1+18 x_1 y_2-18 x_3 y_2-18 x_1 y_3+18 x_2 y_3
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>That is... unwieldy. So, we note that there are a lot of terms that involve multiplications
|
||||
involving x1, y1, and y4, which would all disappear if we axis-align our curve, which is why
|
||||
aligning is a great idea.</p>
|
||||
</div>
|
||||
|
||||
<p>Aligning our curve so that three of the eight coefficients become zero, we end up with the
|
||||
following simple term function for <i>C(t)</i>:</p>
|
||||
|
||||
<p>\[
|
||||
18 \left ( (3 x_3 y_2+2 x_4 y_2+3 x_2 y_3-x_4 y_3)t^2 + (3 x_3 y_2-x_4 y_2-3 x_2 y_3)t + (x_2 y_3-x_3 y_2) \right )
|
||||
\]</p>
|
||||
|
||||
<p>That's a lot easier to work with: we see a fair number of terms that we can compute and then cache,
|
||||
giving us the following simplification:</p>
|
||||
|
||||
<p>\[
|
||||
\left.\begin{matrix}
|
||||
a = x_3 \cdot y_2 \\
|
||||
b = x_4 \cdot y_2 \\
|
||||
c = x_2 \cdot y_3 \\
|
||||
d = x_4 \cdot y_3
|
||||
\end{matrix}\right\}
|
||||
\ C(t) = 18 \cdot (-3a + 2b + 3c - d)t^2 + (3a - b - 3c)t + (c - a)
|
||||
\]</p>
|
||||
|
||||
<p>This is a plain quadratic curve, and we know how to solve <i>C(t) = 0</i>; we use the quadratic
|
||||
formula:</p>
|
||||
|
||||
<p>\[
|
||||
\left.\begin{matrix}
|
||||
x =& 18(-3a + 2b + 3c - d) \\
|
||||
y =& 18(3a - b - 3c) \\
|
||||
z =& 18(c - a)
|
||||
\end{matrix}\right\}
|
||||
\ C(t) = 0 \ \Rightarrow\ t = \frac{-y \pm \sqrt{y^2 - 4 x z}}{2x}
|
||||
\]</p>
|
||||
|
||||
<p>We can easily compute this value <em>if</em> the descriminator isn't a negative number (because
|
||||
we only want real roots, not complex roots), and <em>if</em> v<sub>1</sub> is not zero, because
|
||||
divisions by zero are rather useless.</p>
|
||||
|
||||
<p>Taking that into account, we compute <i>t</i>, we disregard any <i>t</i> value that isn't in
|
||||
the Bézier interval [0,1], and we now know at which <i>t</i> value(s) our curve will inflect.</p>
|
||||
|
||||
<Graphic title="Finding cubic Bézier curve inflections" setup={this.setupCubic} draw={this.draw}/>
|
||||
</section>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "introduction";
|
||||
|
||||
var Introduction = React.createClass({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "matrix";
|
||||
|
||||
var Matrix = React.createClass({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "matrixsplit";
|
||||
|
||||
var MatrixSplit = React.createClass({
|
||||
|
78
components/sections/pointvectors/content.en-GB.md
Normal file
78
components/sections/pointvectors/content.en-GB.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Tangents and normals
|
||||
|
||||
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:
|
||||
|
||||
\[
|
||||
\left \{ \begin{matrix}
|
||||
tangent_x(t) = B'_x(t) \\
|
||||
tangent_y(t) = B'_y(t)
|
||||
\end{matrix} \right.
|
||||
\]
|
||||
|
||||
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:
|
||||
|
||||
\[
|
||||
d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2}
|
||||
\]
|
||||
|
||||
\[
|
||||
\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.
|
||||
\]
|
||||
|
||||
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:
|
||||
|
||||
\[
|
||||
\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.
|
||||
\]
|
||||
|
||||
<div className="note">
|
||||
|
||||
Rotating coordinates is actually very easy, if you know the rule for it. You might find it explained as "applying a [rotation matrix](https://en.wikipedia.org/wiki/Rotation_matrix), 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 these 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.
|
||||
|
||||
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:
|
||||
|
||||
\[\begin{array}{l}
|
||||
x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\
|
||||
y' = x \cdot \sin(\phi) + y \cdot \cos(\phi)
|
||||
\end{array}\]
|
||||
|
||||
Which is the "long" version of the following matrix transformation:
|
||||
|
||||
\[
|
||||
\begin{bmatrix}
|
||||
x' \\ y'
|
||||
\end{bmatrix}
|
||||
=
|
||||
\begin{bmatrix}
|
||||
\cos(\phi) & -\sin(\phi) \\
|
||||
\sin(\phi) & \cos(\phi)
|
||||
\end{bmatrix}
|
||||
\begin{bmatrix}
|
||||
x \\ y
|
||||
\end{bmatrix}
|
||||
\]
|
||||
|
||||
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 *sin* and *cos* for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.
|
||||
|
||||
But ***why*** does this work? Why this matrix multiplication? [Wikipedia](http://en.wikipedia.org/wiki/Rotation_matrix#Decomposition_into_shears) (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. [DataGenetics](http://datagenetics.com/blog/august32013/index.html) 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.
|
||||
|
||||
</div>
|
||||
|
||||
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 (the markers are spaced out evenly as *t*-intervals, not spaced equidistant).
|
||||
|
||||
<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>
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "pointvectors";
|
||||
|
||||
var PointVectors = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Tangents and normals"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -42,107 +44,7 @@ var PointVectors = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<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 (the markers are spaced out
|
||||
evenly as <i>t</i>-intervals, not spaced equidistant).</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>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "preface";
|
||||
|
||||
var Preface = React.createClass({
|
||||
|
25
components/sections/reordering/content.en-GB.md
Normal file
25
components/sections/reordering/content.en-GB.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Lowering and elevating curve order
|
||||
|
||||
One interesting property of Bézier curves is that an *n<sup>th</sup>* order curve can always be perfectly represented by an *(n+1)<sup>th</sup>* order curve, by giving the higher order curve specific control points.
|
||||
|
||||
If we have a curve with three points, then we can create a four point curve that exactly reproduce the original curve as long as we give it the same start and end points, and for its two control points we pick "1/3<sup>rd</sup> start + 2/3<sup>rd</sup> control" and "2/3<sup>rd</sup> control + 1/3<sup>rd</sup> end", and now we have exactly the same curve as before, except represented as a cubic curve, rather than a quadratic curve.
|
||||
|
||||
The general rule for raising an *n<sup>th</sup>* order curve to an *(n+1)<sup>th</sup>* order curve is as follows (observing that the start and end weights are the same as the start and end weights for the old curve):
|
||||
|
||||
\[
|
||||
Bézier(k,t) = \sum_{i=0}^{k}
|
||||
\underset{binomial\ term}{\underbrace{\binom{k}{i}}}
|
||||
\cdot\
|
||||
\underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
|
||||
\ \cdot \
|
||||
\underset{new\ weights}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}}
|
||||
\ ,\ with\ k = n+1\ and\ w_{i-1}=0\ when\ i = 0
|
||||
\]
|
||||
|
||||
However, this rule also has as direct consequence that you **cannot** generally safely lower a curve from *n<sup>th</sup>* order to *(n-1)<sup>th</sup>* order, because the control points cannot be "pulled apart" cleanly. We can try to, but the resulting curve will not be identical to the original, and may in fact look completely different.
|
||||
|
||||
We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch and press your up and down arrow keys to elevate or lower the curve order.
|
||||
|
||||
<Graphic preset="simple" title={"A " + this.getOrder() + " order Bézier curve"} setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
There is a good, if mathematical, explanation on the matrices necessary for optimal reduction over on [Sirver's Castle](http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves), which given time will find its way in a more direct description into this article.
|
@@ -1,9 +1,17 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "reordering";
|
||||
|
||||
var Reordering = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
statics: {
|
||||
// Improve this based on http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
|
||||
lower: function(curve) {
|
||||
@@ -34,12 +42,6 @@ var Reordering = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Lowering and elevating curve order"
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
order: 0
|
||||
@@ -116,48 +118,7 @@ var Reordering = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>One interesting property of Bézier curves is that an <i>n<sup>th</sup></i> order curve can
|
||||
always be perfectly represented by an <i>(n+1)<sup>th</sup></i> order curve, by giving the
|
||||
higher order curve specific control points.</p>
|
||||
|
||||
<p>If we have a curve with three points, then we can create a four point curve that exactly
|
||||
reproduce the original curve as long as we give it the same start and end points, and for
|
||||
its two control points we pick "1/3<sup>rd</sup> start + 2/3<sup>rd</sup> control" and
|
||||
"2/3<sup>rd</sup> control + 1/3<sup>rd</sup> end", and now we have exactly the same curve as
|
||||
before, except represented as a cubic curve, rather than a quadratic curve.</p>
|
||||
|
||||
<p>The general rule for raising an <i>n<sup>th</sup></i> order curve to an <i>(n+1)<sup>th</sup></i>
|
||||
order curve is as follows (observing that the start and end weights are the same as the start and
|
||||
end weights for the old curve):</p>
|
||||
|
||||
<p>\[
|
||||
Bézier(k,t) = \sum_{i=0}^{k}
|
||||
\underset{binomial\ term}{\underbrace{\binom{k}{i}}}
|
||||
\cdot\
|
||||
\underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
|
||||
\ \cdot \
|
||||
\underset{new\ weights}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}}
|
||||
\ ,\ with\ k = n+1\ and\ w_{i-1}=0\ when\ i = 0
|
||||
\]</p>
|
||||
|
||||
<p>However, this rule also has as direct consequence that you <strong>cannot</strong> generally
|
||||
safely lower a curve from <i>n<sup>th</sup></i> order to <i>(n-1)<sup>th</sup></i> order, because
|
||||
the control points cannot be "pulled apart" cleanly. We can try to, but the resulting curve will
|
||||
not be identical to the original, and may in fact look completely different.</p>
|
||||
|
||||
<p>We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch
|
||||
and press your up and down arrow keys to elevate or lower the curve order.</p>
|
||||
|
||||
<Graphic preset="simple" title={"A " + this.getOrder() + " order Bézier curve"} setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
<p>There is a good, if mathematical, explanation on the matrices necessary for optimal reduction
|
||||
over on <a href="http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/">Sirver's Castle</a>,
|
||||
which given time will find its way in a more direct description into this article.</p>
|
||||
|
||||
</section>
|
||||
<section>{ locale.getContent(page, this) }</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "splitting";
|
||||
|
||||
var Splitting = React.createClass({
|
||||
|
8
components/sections/tightbounds/content.en-GB.md
Normal file
8
components/sections/tightbounds/content.en-GB.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Tight boxes
|
||||
|
||||
With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for curves. We first align our curve, recording the translation we performed, "T", and the rotation angle we used, "R". We then determine the aligned curve's normal bounding box. Once we have that, we can map that bounding box back to our original curve by rotating it by -R, and then translating it by -T. We now have nice tight bounding boxes for our curves:
|
||||
|
||||
<Graphic preset="twopanel" title="Aligning a quadratic curve" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="twopanel" title="Aligning a cubic curve" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding precision is often not worth it. If there is high demand for it, I'll add a section on how to precisely compute the best fit bounding box, but the maths is fairly gruelling and just not really worth spending time on.
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "tightbounds";
|
||||
|
||||
var TightBounds = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Tight boxes"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -80,27 +82,7 @@ var TightBounds = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for
|
||||
curves. We first align our curve, recording the translation we performed, "T", and the rotation angle we
|
||||
used, "R". We then determine the aligned curve's normal bounding box. Once we have that, we can map that
|
||||
bounding box back to our original curve by rotating it by -R, and then translating it by -T. We now have
|
||||
nice tight bounding boxes for our curves:</p>
|
||||
|
||||
<Graphic preset="twopanel" title="Aligning a quadratic curve" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="twopanel" title="Aligning a cubic curve" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
<p>These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute
|
||||
the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because
|
||||
of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding
|
||||
precision is often not worth it. If there is high demand for it, I'll add a section on how to precisely compute
|
||||
the best fit bounding box, but the maths is fairly gruelling and just not really worth spending time on.</p>
|
||||
|
||||
</section>
|
||||
);
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var React = require("react");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale("en-GB");
|
||||
var locale = new Locale();
|
||||
var page = "whatis";
|
||||
|
||||
var Whatis = React.createClass({
|
||||
|
Reference in New Issue
Block a user