1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-19 06:52:02 +02:00

1/3rd there

This commit is contained in:
Pomax
2016-01-01 17:52:50 -08:00
parent 71c42b8a2f
commit 3ba0ed40ae
35 changed files with 7090 additions and 156 deletions

View File

@@ -276,37 +276,3 @@ function Bezier(3,t,w[]):
});
module.exports = Control;
/**
<textarea class="sketch-code" data-sketch-preset="ratios" data-sketch-title="Quadratic interpolations">
int order = 3;
</textarea>
<textarea class="sketch-code" data-sketch-preset="ratios" data-sketch-title="Cubic interpolations">
int order = 4;
</textarea>
<textarea class="sketch-code" data-sketch-preset="ratios" data-sketch-title="15th order interpolations">
int order = 15;
</textarea>
**/
/**
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
}</Graphic>
**/

View File

@@ -0,0 +1,101 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var Flattening = React.createClass({
statics: {
title: "Simplified drawing"
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 3;
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 5;
},
drawFlattened: function(api, curve) {
api.reset();
api.setColor("#DDD");
api.drawSkeleton(curve);
api.setColor("#DDD");
api.drawCurve(curve);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
api.setFill("black");
api.text("Curve approximation using "+api.steps+" segments", {x:10, y:15});
},
values: {
"+": 1,
"-": -1
},
onKeyDown: function(e, api) {
e.preventDefault();
var v = this.values[e.key];
if(v) {
api.steps += v;
if (api.steps < 1) {
api.steps = 1;
}
}
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ Flattening.title }</SectionHeader>
<p>We can also simplify the drawing process by "sampling" the curve at certain points, and then joining those points up with straight lines, a process known as "flattening", as we are reducing a curve to a simple sequence of straight, "flat" lines.</p>
<p>We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.</p>
<Graphic preset="twopanel" title="Flattening a quadratic curve" setup={this.setupQuadratic} draw={this.drawFlattened} onKeyDown={this.onKeyDown}/>
<Graphic preset="twopanel" title="Flattening a cubic curve" setup={this.setupCubic} draw={this.drawFlattened} onKeyDown={this.onKeyDown} />
<p>Try clicking on the sketch and using your '+' and '-' keys to lower the number of segments for both the quadratic and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.</p>
<div className="howtocode">
<h3>How to implement curve flattening</h3>
<p>Let's just use the algorithm we just specified, and implement that:</p>
<pre>function flattenCurve(curve, segmentCount):
step = 1/segmentCount;
coordinates = [curve.getXValue(0), curve.getYValue(0)]
for(i=1; i &lt;= segmentCount; i++):
t = i*step;
coordinates.push[curve.getXValue(t), curve.getYValue(t)]
return coordinates;</pre>
<p>And done, that's the algorithm implemented. That just leaves drawing the resulting "curve" as a sequence of lines:</p>
<pre>function drawFlattenedCurve(curve, segmentCount):
coordinates = flattenCurve(curve, segmentCount)
coord = coordinates[0], _coords;
for(i=1; i &lt; coordinates.length; i++):
_coords = coordinates[i]
line(coords, _coords)
coords = _coords</pre>
<p>We start with the first coordinate as reference point, and then just draw lines between each point and its next point.</p>
</div>
</section>
);
}
});
module.exports = Flattening;

View File

@@ -9,17 +9,16 @@ module.exports = {
explanation: require("./explanation"),
control: require("./control"),
matrix: require("./matrix"),
decasteljau: require("./decasteljau")
decasteljau: require("./decasteljau"),
flattening: require("./flattening"),
splitting: require("./splitting"),
matrixsplit: require("./matrixsplit"),
reordering: require("./reordering")
};
/*
flattening: require("./flattening"),
splitting: require("./splitting"),
matrixsplit: require("./matrixsplit"),
reordering: require("./reordering"),
derivatives: require("./derivatives"),
pointvectors: require("./pointvectors"),
components: require("./components"),
@@ -53,3 +52,43 @@ module.exports = {
circles_cubic: require("./circles_cubic"),
arcapproximation: require("./arcapproximation")
*/
/*
A lightning introduction
What is a Bézier curve?
The basics of Bézier curves
Controlling Bézier curvatures
Bézier curvatures as matrix operations
de Casteljau's algorithm
Simplified drawing
Splitting curves
Splitting curves using matrices
Lowering and elevating curve order
Derivatives
Tangents and normals
Component functions
Finding extremities
Bounding boxes
Aligning curves
Tight boxes
The canonical form (for cubic curves)
Arc length
Approximated arc length
Tracing a curve at fixed distance intervals
Intersections
Curve/curve intersection
Curve moulding (using the projection ratio)
Creating a curve from three points
Bézier curves and Catmull-Rom curves
Creating a Catmull-Rom curve from three points
Forming poly-Bézier curves
Boolean shape operations
Projecting a point onto a Bézier curve
Curve offsetting
Graduated curve offsetting
Circles and quadratic Bézier curves
Circles and cubic Bézier curves
Approximating Bézier curves with circular arcs
*/

View File

@@ -0,0 +1,631 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var MatrixSplit = React.createClass({
statics: {
title: "Splitting curves using matrices"
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ MatrixSplit.title }</SectionHeader>
<p>Another way to split curves is to exploit the matrix representation of
a Bézier curve. In <a href="#matrix">the section on matrices</a> we saw that
we can represent curves as matrix multiplications. Specifically, we saw these
two forms for the quadratic, and cubic curves, respectively (using the reversed
Bézier coefficients vector for legibility):</p>
<p>\[
B(t) = \begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
B(t) = \begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0\\
-3 & 3 & 0 & 0\\
3 & -6 & 3 & 0\\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>Let's say we want to split the curve at some point <em>t = z</em>, forming
two new (obviously smaller) Bézier curves. To find the coordinates for these
two Bézier curves, we can use the matrix representation and some linear algebra.
First, we split out the the actual "point on the curve" information as a new matrix
multiplication:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & (z \cdot t) & (z \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & (z \cdot t) & (z \cdot t)^2 & (z \cdot t)^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0\\
0 & z & 0 & 0\\
0 & 0 & z^2 & 0\\
0 & 0 & 0 & z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>If we could compact these matrices back to a form <strong>[t values] · [bezier matrix] · [column matrix]</strong>,
with the first two staying the same, then that column matrix on the right would be the coordinates
of a new Bézier curve that describes the first segment, from <em>t = 0</em> to <em>t = z</em>.
As it turns out, we can do this quite easily, by exploiting some simple rules of linear algebra
(and if you don't care about the derivations, just skip to the end of the box for the results!).</p>
<div className="note">
<h2>Deriving new hull coordinates</h2>
<p>Deriving the two segments upon splitting a curve takes a few steps, and the higher
the curve order, the more work it is, so let's look at the quadratic curve first:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\underset{we\ turn\ this...}{\underbrace{Z \cdot M}}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\underset{...into\ this!}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
Q
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>We do this, because [<em>M · M<sup>-1</sup></em>] is the identity matrix (a bit like
multiplying something by x/x in calculus. It doesn't do anything to the function, but it
does allow you to rewrite it to something that may be easier to work with, or can be
broken up differently). Adding that as matrix multiplication has no effect on the total
formula, but it does allow us to change the matrix sequence [<em>something · M</em>] to
a sequence [<em>M · something</em>], and that makes a world of difference: if we know
what [<em>M<sup>-1</sup> · Z · M</em>] is, we can apply that to our coordinates, and be
left with a proper matrix representation of a quadratic Bézier curve (which is
[<em>T · M · P</em>]), with a new set of coordinates that represent the curve from
<em>t = 0</em> to <em>t = z</em>. So let's get computing:</p>
<p>\[
Q = M^{-1} \cdot Z \cdot M =
\begin{bmatrix}
1 & 0 & 0 \\
1 & \frac{1}{2} & 0 \\
1 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
=
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\]</p>
<p>Excellent! Now we can form our new quadratic curve:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot M \cdot Q \cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
\left (
Q
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\left (
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1
\end{bmatrix}
\]</p>
<p><strong><em>Brilliant</em></strong>: if we want a subcurve from <em>t = 0</em>
to <em>t = z</em>, we can keep the first coordinate the same (which makes sense),
our control point becomes a z-ratio mixture of the original control point and the start
point, and the new end point is a mixture that looks oddly similar to a bernstein
polynomial of degree two, except it uses (z-1) rather than (1-z)... These new
coordinates are actually really easy to compute directly!</p>
<p>Of course, that's only one of the two curves. Getting the section from <em>t = z</em>
to <em>t = 1</em> requires doing this again. We first observe what what we just did is
actually evaluate the general interval [0,<em>z</em>], which we wrote down simplified
becuase of that zero, but we actually evaluated this:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & ( 0 + z \cdot t) & ( 0 + z \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>If we want the interval [<em>z</em>,1], we will be evaluating this instead:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & ( z + (1-z) \cdot t) & ( z + (1-z) \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & z & z^2 \\
0 & 1-z & 2 \cdot z \cdot (1-z) \\
0 & 0 & (1-z)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>We're going to do the same trick, to turn <em>[something · M]</em> into <em>[M · something]</em>:</p>
<p>\[
Q' = M^{-1} \cdot Z' \cdot M =
\begin{bmatrix}
1 & 0 & 0 \\
1 & \frac{1}{2} & 0 \\
1 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & z & z^2 \\
0 & 1-z & 2 \cdot z \cdot (1-z) \\
0 & 0 & (1-z)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
=
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\]</p>
<p>So, our final second curve looks like:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot M \cdot Q \cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
\left (
Q'
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\left (
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z \cdot P_3 - (z-1) \cdot P_2 \\
P_3
\end{bmatrix}
\]</p>
<p><strong><em>Nice</em></strong>: we see the same as before; can keep the last
coordinate the same (which makes sense), our control point becomes a z-ratio
mixture of the original control point and the end point, and the new start point
is a mixture that looks oddly similar to a bernstein polynomial of degree two,
except it uses (z-1) rather than (1-z). These new coordinates are <em>also</em>
really easy to compute directly!</p>
</div>
<p>So, using linear algebra rather than de Casteljau's algorithm, we have determined
that for any quadratic curve split at some value <em>t = z</em>, we get two subcurves
that are described as Bézier curves with simple-to-derive coordinates.</p>
<p>\[
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z \cdot P_3 - (z-1) \cdot P_2 \\
P_3
\end{bmatrix}
\]</p>
<p>We can do the same for cubic curves. However, I'll spare you the actual derivation
(don't let that stop you from writing that out yourself, though) and simply show you
the resulting new coordinate sets:</p>
<p>\[
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-(z-1) & z & 0 & 0 \\
(z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 & 0 \\
-(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
\begin{bmatrix}
-(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1)^3 \cdot z^2 & z^3 \\
0 & (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 \\
0 & 0 & -(z-1) & z \\
0 & 0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \\
z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \\
z \cdot P_4 - (z-1) \cdot P_3 \\
P_4
\end{bmatrix}
\]</p>
<p>So, looking at our matrices, did we really need to compute the second segment matrix?
No, we didn't. Actually having one segment's matrix means we implicitly have the other:
push the values of each row in the matrix <strong><em>Q</em></strong> to the right, with
zeroes getting pushed off the right edge and appearing back on the left, and then flip
the matrix vertically. Presto, you just "calculated" <strong><em>Q'</em></strong>.</p>
<p>Implementing curve splitting this way requires less recursion, and is just straight
arithmetic with cached values, so can be cheaper on systems were recursion is expensive.
If you're doing computation with devices that are good at matrix multiplication, chopping
up a Bézier curve with this method will be a lot faster than applying de Casteljau.</p>
</section>
);
}
});
module.exports = MatrixSplit;

View File

@@ -0,0 +1,182 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var Reordering = React.createClass({
statics: {
title: "Lowering and elevating curve order"
},
getInitialState: function() {
return {
order: 0
};
},
setup: function(api) {
var points = [];
var w = api.getPanelWidth(),
h = api.getPanelHeight();
for (var i=0; i<10; i++) {
points.push({
x: w/2 + (Math.random() * 20) + Math.cos(Math.PI*2 * i/10) * (w/2 - 40),
y: h/2 + (Math.random() * 20) + Math.sin(Math.PI*2 * i/10) * (h/2 - 40)
});
}
var curve = new api.Bezier(points);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
var pts = curve.points;
this.setState({
order: pts.length
});
var p0 = pts[0];
// we can't "just draw" this curve, since it'll be an arbitrary order,
// And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm:
for(var t=0; t<=1; t+=0.01) {
var q = JSON.parse(JSON.stringify(pts));
while(q.length > 1) {
for (var i=0; i<q.length-1; i++) {
q[i] = {
x: q[i].x + (q[i+1].x - q[i].x) * t,
y: q[i].y + (q[i+1].y - q[i].y) * t
}
}
q.splice(q.length-1, 1);
}
api.drawLine(p0, q[0]);
p0 = q[0];
}
p0 = pts[0];
api.setColor("black");
api.drawCircle(p0,3);
pts.forEach(p => {
if(p===p0) return;
api.setColor("#DDD");
api.drawLine(p0,p);
api.setColor("black");
api.drawCircle(p,3);
p0 = p;
});
},
// Improve this based on http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
lower: function(curve) {
var pts = curve.points, q=[], n = pts.length;
pts.forEach((p,k) => {
if (!k) { return (q[k] = p); }
var f1 = k/n, f2 = 1 - f1;
q[k] = {
x: f1 * p.x + f2 * pts[k-1].x,
y: f1 * p.y + f2 * pts[k-1].y
};
});
q.splice(n-1,1);
q[n-2] = pts[n-1];
curve.points = q;
return curve;
},
onKeyDown: function(evt, api) {
evt.preventDefault();
if(evt.key === "ArrowUp") {
api.setCurve(api.curve.raise());
} else if(evt.key === "ArrowDown") {
api.setCurve(this.lower(api.curve));
}
},
getOrder: function() {
var order = this.state.order;
if (order%10 === 1 && order !== 11) {
order += "st";
} else if (order%10 === 2 && order !== 12) {
order += "nd";
} else if (order%10 === 3 && order !== 13) {
order += "rd";
} else {
order += "th";
}
return order;
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ Reordering.title }</SectionHeader>
<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
\]</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 cursor 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.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>
);
}
});
module.exports = Reordering;
/*
void setupCurve() {
int d = dim - 2*pad;
int order = 10;
ArrayList<Point> pts = new ArrayList<Point>();
float dst = d/2.5, nx, ny, a=0, step = 2*PI/order, r;
for(a=0; a<2*PI; a+=step) {
r = random(-dst/4,dst/4);
pts.add(new Point(d/2 + cos(a) * (r+dst), d/2 + sin(a) * (r+dst)));
dst -= 1.2;
}
Point[] points = new Point[pts.size()];
for(int p=0,last=points.length; p<last; p++) { points[p] = pts.get(p); }
curves.add(new BezierCurve(points));
reorder();
}
void drawCurve(BezierCurve curve) {
curve.draw();
}</textarea>
*/

View File

@@ -0,0 +1,150 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var Splitting = React.createClass({
statics: {
title: "Splitting curves"
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.forward = true;
},
drawSplit: function(api, curve) {
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var offset = {x:0, y:0};
var t = 0.5;
var pt = curve.get(0.5);
var split = curve.split(t);
api.drawCurve(split.left);
api.drawCurve(split.right);
api.setColor("red");
api.drawCircle(pt,3);
api.setColor("black");
offset.x = api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawCircle(pt,4);
offset.x -= 20;
offset.y -= 20;
api.drawSkeleton(split.left, offset, true);
api.drawCurve(split.left, offset);
offset.x += 40;
offset.y += 40;
api.drawSkeleton(split.right, offset, true);
api.drawCurve(split.right, offset);
},
drawAnimated: function(api, curve) {
api.setPanelCount(3);
api.reset();
var frame = api.getFrame();
var interval = 5 * api.getPlayInterval();
var t = (frame%interval)/interval;
var forward = (frame%(2*interval)) < interval;
if (forward) { t = t%1; } else { t = 1 - t%1; }
var offset = {x:0, y:0};
api.setColor("lightblue");
api.drawHull(curve, t);
api.drawSkeleton(curve);
api.drawCurve(curve);
var pt = curve.get(t);
api.drawCircle(pt, 4);
api.setColor("black");
offset.x += api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
var split = curve.split(t);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawHull(curve, t, offset);
api.setColor("black");
api.drawCurve(split.left, offset);
api.drawPoints(split.left.points, offset);
api.setFill("black");
api.text("Left side of curve split at t = " + (((100*t)|0)/100), {x: 10 + offset.x, y: 15 + offset.y});
offset.x += api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawHull(curve, t, offset);
api.setColor("black");
api.drawCurve(split.right, offset);
api.drawPoints(split.right.points, offset);
api.setFill("black");
api.text("Right side of curve split at t = " + (((100*t)|0)/100), {x: 10 + offset.x, y: 15 + offset.y});
},
togglePlay: function(evt, api) {
if (api.playing) { api.pause(); } else { api.play(); }
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>{ Splitting.title }</SectionHeader>
<p>With de Casteljau's algorithm we also find all the points we need to split up a Bézier curve into two, smaller
curves, which taken together form the original curve. When we construct de Casteljau's skeleton for some value
<i>t</i>, the procedure gives us all the points we need to split a curve at that <i>t</i> value: one curve is defined
by all the inside skeleton points found prior to our on-curve point, with the other curve being defined by all the
inside skeleton points after our on-curve point.</p>
<Graphic title="Splitting a curve" setup={this.setupCubic} draw={this.drawSplit} />
<div className="howtocode">
<h3>implementing curve splitting</h3>
<p>We can implement curve splitting by bolting some extra logging onto the de Casteljau function:</p>
<pre>left=[]
right=[]
function drawCurve(points[], t):
if(points.length==1):
left.add(points[0])
right.add(points[0])
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
if(i==0):
left.add(points[i])
if(i==newpoints.length-1):
right.add(points[i+1])
newpoints[i] = (1-t) * points[i] + t * points[i+1]
drawCurve(newpoints, t)</pre>
<p>After running this function for some value <i>t</i>, the <i>left</i> and <i>right</i> arrays
will contain all the coordinates for two new curves - one to the "left" of our <i>t</i> value,
the other on the "right", of the same order as the original curve, and overlayed exactly on the
original curve.</p>
</div>
<p>This is best illustrated with an animated graphic (click to play/pause):</p>
<Graphic preset="threepanel" title="Bézier curve splitting" setup={this.setupCubic} draw={this.drawAnimated} onClick={this.togglePlay} />
</section>
);
}
});
module.exports = Splitting;