mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-30 03:30:34 +02:00
catmull
This commit is contained in:
@@ -7,7 +7,7 @@ var abs = Math.abs;
|
||||
var ABC = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "The 'ABC' curve identity"
|
||||
title: "The projection identity"
|
||||
};
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ var ABC = React.createClass({
|
||||
|
||||
api.setFill("black");
|
||||
api.text("A", {x:10 + A.x, y: A.y});
|
||||
api.text("B", {x:10 + B.x, y: B.y});
|
||||
api.text("B (t = " + api.utils.round(api.t,2) + ")", {x:10 + B.x, y: B.y});
|
||||
api.text("C", {x:10 + C.x, y: C.y});
|
||||
|
||||
var d1 = utils.dist(A, B);
|
||||
@@ -80,6 +80,59 @@ var ABC = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
setCT: function(evt,api) {
|
||||
api.t = evt.offsetX / api.getPanelWidth();
|
||||
},
|
||||
|
||||
drawCTgraph: function(api) {
|
||||
api.reset();
|
||||
api.setColor("black");
|
||||
var w = api.getPanelWidth();
|
||||
var h = api.getPanelHeight();
|
||||
var pad = 20;
|
||||
var fwh = w - 2*pad;
|
||||
api.drawAxes(pad, "t",0,1, "u",0,1);
|
||||
api.setColor("blue");
|
||||
var uPoint = function(t) {
|
||||
var value = api.u(t),
|
||||
res = { x: pad + t*fwh, y: pad + value*fwh };
|
||||
return res;
|
||||
}
|
||||
api.drawFunction(uPoint);
|
||||
if (api.t) {
|
||||
var v = api.u(api.t),
|
||||
v1 = api.utils.round(v,3),
|
||||
v2 = api.utils.round(1-v,3),
|
||||
up = uPoint(api.t);
|
||||
api.drawLine({x:up.x,y:pad}, up);
|
||||
api.drawLine({x:pad,y:up.y}, up);
|
||||
api.drawCircle(up,3);
|
||||
api.setFill("blue");
|
||||
api.text(" t = " + api.utils.round(api.t,3), {x:up.x+10, y:up.y-7});
|
||||
api.text("u(t) = " + api.utils.round(v,3), {x:up.x+10, y:up.y+7});
|
||||
api.setFill("black");
|
||||
api.text("C = "+v1+" * start + "+v2+" * end", {x:w/2 - pad, y:pad+fwh});
|
||||
}
|
||||
},
|
||||
|
||||
drawQCT: function(api) {
|
||||
api.u = api.u || function(t) {
|
||||
var top = (t-1) * (t-1),
|
||||
bottom = 2*t*t - 2*t + 1;
|
||||
return top/bottom;
|
||||
};
|
||||
this.drawCTgraph(api);
|
||||
},
|
||||
|
||||
drawCCT: function(api) {
|
||||
api.u = api.u || function(t) {
|
||||
var top = (1-t) * (1-t) * (1-t),
|
||||
bottom = t*t*t + top;
|
||||
return top/bottom;
|
||||
};
|
||||
this.drawCTgraph(api);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
@@ -92,16 +145,15 @@ var ABC = React.createClass({
|
||||
|
||||
<p>How does that work? Succinctly: we run de Casteljau's algorithm in reverse!</p>
|
||||
|
||||
<p>Let's start out with a pre-existing curve, defined by <i>start</i>, two control points, and <i>end</i>. We can
|
||||
mould this curve by picking a point somewhere on the curve, at some <i>t</i> value, and the moving it to a new
|
||||
location and reconstructing the curve that goes through <i>start</i>, our new point with the original tangent,
|
||||
and <i>end</i>. In order to see how and why we can do this, let's look at some identity information for Bézier
|
||||
curves. There's actually a hidden goldmine of identities that we can exploit when doing Bézier operations, and
|
||||
this will only scratch the surface. But, in a good way!</p>
|
||||
<p>In order to run de Casteljau's algorithm in reverse, we need a few basic things: a start and end point, a point
|
||||
on the curve that want to be moving around, which has an associated <i>t</i> value, and a point we've not explicitly
|
||||
talked about before, and as far as I know has no explicit name, but lives one iteration higher in the de Casteljau
|
||||
process then our on-curve point does. I like to call it "A" for reasons that will become obvious.</p>
|
||||
|
||||
<p>In the following graphic, click anywhere on the curves to see the identity information that we'll
|
||||
be using to run de Casteljau in reverse (you can manipulate the curve even after picking a point.
|
||||
Note the "ratio" value when you do so: does it change?):</p>
|
||||
<p>So let's use graphics instead of text to see where this "A" is, because text only gets us so far: in the
|
||||
following graphic, click anywhere on the curves to see the identity information that we'll be using to run
|
||||
de Casteljau in reverse (you can manipulate the curve even after picking a point. Note the "ratio" value
|
||||
when you do so: does it change?):</p>
|
||||
|
||||
<div className="figure">
|
||||
<Graphic inline={true} preset="abc" title="Projections in a quadratic Bézier curve"
|
||||
@@ -110,52 +162,81 @@ var ABC = React.createClass({
|
||||
setup={this.setupCubic} draw={this.draw} onClick={this.onClick} />
|
||||
</div>
|
||||
|
||||
<p>So, what exactly do we see in these graphics? First off, there's the three points <i>A</i>, <i>B</i> and <i>C</i>.</p>
|
||||
<p>Clicking anywhere on the curves shows us three things:</p>
|
||||
|
||||
<p>Point <i>B</i> is our "on curve" point, A is the first "strut" point when running de Casteljau's
|
||||
algorithm in reverse; for quadratic curves, this happens to also be the curve's control point. For cubic
|
||||
curves, it's the "top of the triangle" for the struts that lead to point <i>B</i>. Point <i>C</i>, finally,
|
||||
is the intersection of the line that goes through <i>A</i> and <i>B</i> and the baseline,
|
||||
between our start and end points.</p>
|
||||
<ol>
|
||||
<li>our on-curve point; let's call that <b>B</b>,</li>
|
||||
<li>a point at the tip of B's "hat", on de Casteljau step up; let's call that <b>A</b>, and</li>
|
||||
<li>a point that we get by projecting B onto the start--end baseline; let's call that <b>C</b>.</li>
|
||||
</ol>
|
||||
|
||||
<p>There is some important identity information here: as long as we don't pick a new <i>t</i> coordinate,
|
||||
the location of point <i>C</i> on the line <i>start-end</i> represents a fixed ratio distance. We can drag
|
||||
around the control points as much as we like, that point won't move at all, and if we can drag around
|
||||
the start or end point, C will stay at the same ratio-value. For instance, if it was located midway between
|
||||
start and end, it'll stay midway between start and end, even if the line segment between start and end
|
||||
becomes longer or shorter.</p>
|
||||
<p>These three values ABC hide an important identity formula for quadratic and cubic Bézier curves:
|
||||
for any point on the curve with some <i>t</i> value, the ratio distance of C along baseline is fixed:
|
||||
if some <i>t</i> value sets up a C that is 20% away from the start and 80% away from the end, then it
|
||||
doesn't matter where the start, end, or control points are: for that <i>t</i> value, C will <em>always</em> lie
|
||||
at 20% from the start and 80% from the end point. Go ahead, pick an on-curve point in either graphic
|
||||
and then move all the other points around: if you only move the control points, start and end won't move,
|
||||
and so neither will C, and if you move either start or end point, C will move but its relative position
|
||||
will not change. The following function stays true:</p>
|
||||
|
||||
<p>We can also see that the distances for the lines <i>d1 = A-B</i> and <i>d2 = B-C</i> may vary, but the
|
||||
ratio between them, <i>d1/d2</i>, is a constant value. We can drag any of the start, end, or control points
|
||||
around as much as we like, but that value also stays the same.</p>
|
||||
<p>\[
|
||||
C = u \cdot P_{start} + (1-u) \cdot P_{end}
|
||||
\]</p>
|
||||
|
||||
<p>So that just leaves finding A.</p>
|
||||
|
||||
<div className="note">
|
||||
<p>In fact, because the distance ratio is a fixed value for each point <i>B</i>, which we get by picking
|
||||
some <i>t</i> value on our curve, the distance ratio is actually an identity function for Bézier curves.
|
||||
If we were to plot all the ratio values for all possible <i>t</i> values for quadratic and cubic curves,
|
||||
we'd see two very interesting functions: asymptotic at <i>t=0</i> and <i>t=1</i>, tending towards positive
|
||||
infinity, with a zero-derivative minimum at <i>t=0.5</i>.</p>
|
||||
<p>While that relation is fixed, the function <i>u(t)</i> differs depending on whether we're working
|
||||
with quadratic or cubic curves:</p>
|
||||
|
||||
<p>Since these are ratios, we can actually express the ratio values as a function of <i>t</i>. I actually
|
||||
failed at coming up with the precise functions, but thanks to some help from
|
||||
<a href="http://mathoverflow.net/questions/122257/finding-the-formula-for-Bézier-curve-ratios-hull-point-point-baseline">Boris
|
||||
Zbarsky</a> we can see that the ratio functions are actually remarkably simple:</p>
|
||||
<p>\[\begin{align}
|
||||
& u(t)_{quadratic} = \frac{(t-1)^2}{2t^2 - 2t + 1} \\
|
||||
& u(t)_{cubic} = \frac{(1-t)^3}{t^3 + (1-t)^3}
|
||||
\end{align}\]</p>
|
||||
|
||||
<table style={{width:"100%", border:0}}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Quadratic curves:\[
|
||||
ratio(t)_2 = \left | \frac{2t^2 - 2t}{2t^2 - 2t + 1} \right |
|
||||
\]</p>
|
||||
</td><td>
|
||||
<p>Cubic curves: \[
|
||||
ratio(t)_3 = \left | \frac{t^3 + (1-t)^3 - 1}{t^3 + (1-t)^3} \right |
|
||||
\]</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>So, if we know the start and end coordinates, and we know the <i>t</i> value, we know C:</p>
|
||||
|
||||
<div className="figure">
|
||||
<Graphic inline={true} preset="abc" title="Quadratic value of C for t" draw={this.drawQCT} onMouseMove={this.setCT}/>
|
||||
<Graphic inline={true} preset="abc" title="Cubic value of C for t" draw={this.drawCCT} onMouseMove={this.setCT}/>
|
||||
</div>
|
||||
|
||||
<p>Mouse-over the graphs to see the expression for C, given the <i>t</i> value at the mouse pointer.</p>
|
||||
</div>
|
||||
|
||||
<p>There's also another important bit of information that is inherent to the ABC values: while the distances
|
||||
between A and B, and B and C, are dynamic (based on where we put B), the <em>ratio</em> between the two
|
||||
distances is stable: given some <i>t</i> value, the following always holds:</p>
|
||||
|
||||
<p>\[
|
||||
ratio(t) = \frac{distance(B,C)}{distance(A,B)} = Constant
|
||||
\]</p>
|
||||
|
||||
<p>This leads to a pretty powerful bit of knowledge: merely by knowing the <i>t</i> value of some on curve
|
||||
point, we know where C has to be (as per the above note), and because we know B and C, and thus have the
|
||||
distance between them, we know where A has to be:</p>
|
||||
|
||||
<p>\[
|
||||
A = B - \frac{C - B}{ratio(t)} = B + \frac{B - C}{ratio(t)}
|
||||
\]</p>
|
||||
|
||||
<p>And that's it, all values found.</p>
|
||||
|
||||
<div className="note">
|
||||
<p>Much like the <i>u(t)</i> function in the above note, the <i>ratio(t)</i> function depends
|
||||
on whether we're looking at quadratic or cubic curves. Their form is intrinsically related to
|
||||
the <i>u(t)</i> function in that they both come rolling out of the same function evalution,
|
||||
explained over on <a href="http://mathoverflow.net/questions/122257/finding-the-formula-for-Bézier-curve-ratios-hull-point-point-baseline">MathOverflow</a> by
|
||||
Boris Zbarsky and myself. The ratio functions are the "s(t)" functions from the answers there,
|
||||
while the "u(t)" functions have the same name both here and on MathOverflow.</p>
|
||||
|
||||
<p>\[
|
||||
ratio(t)_{quadratic} = \left | \frac{2t^2 - 2t}{2t^2 - 2t + 1} \right |
|
||||
\]</p>
|
||||
|
||||
<p>\[
|
||||
ratio(t)_{cubic} = \left | \frac{t^3 + (1-t)^3 - 1}{t^3 + (1-t)^3} \right |
|
||||
\]</p>
|
||||
|
||||
<p>Unfortunately, this trick only works for quadratic and cubic curves. Once we hit higher order curves,
|
||||
things become a lot less predictable; the "fixed point <i>C</i>" is no longer fixed, moving around as we
|
||||
@@ -163,6 +244,11 @@ var ABC = React.createClass({
|
||||
lie on that line before the start, or after the end, and there are no simple ratios that we can exploit.</p>
|
||||
</div>
|
||||
|
||||
<p>So: if we know B and its corresponding <i>t</i> value, then we know all the ABC values, which
|
||||
—together with a start and end coordinate— gives us the necessary information to reconstruct a curve's
|
||||
"de Casteljau skeleton", which means that two points and a value between 0 and 1, we can come up with
|
||||
a curve. And that opens up possibilities: curve manipulation by dragging an on-curve point, curve fitting
|
||||
of "a bunch of coordinates", these are useful things, and we'll look at both in the next sections.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
417
components/sections/catmullconv/index.js
Normal file
417
components/sections/catmullconv/index.js
Normal file
@@ -0,0 +1,417 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var CatmullRomConversion = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Bézier curves and Catmull-Rom curves"
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Taking an excursion to different splines, the other common design curve is
|
||||
the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom
|
||||
spline</a>. Now, a Catmull-Rom spline is a form of cubic Hermite spline, and as it so happens
|
||||
the cubic Bézier curve is also a cubic Hermite spline, so maybe... maybe we can convert one
|
||||
into the other, and back, with some simple substitutions?</p>
|
||||
|
||||
<p>Unlike Bézier curves, Catmull-Rom splines pass through each point used to define the curve,
|
||||
except the first and last, which makes sense if you read the "natural language" description
|
||||
for how a Catmull-Rom spline works: a Catmull-Rom spline is a curve that, at each point
|
||||
P<sub>x</sub>, has a tangent along the line P<sub>x-1</sub> to P<sub>x+1</sub>. The curve
|
||||
runs from points P<sub>2</sub> to P<sub>n-1</sub>, and has a "tension" that determines
|
||||
how fast the curve passes through each point. The lower the tension, the faster the curve
|
||||
goes through each point, and the bigger its local tangent is.</p>
|
||||
|
||||
<p>I'll be showing the conversion to and from Catmull-Rom curves for the tension that the
|
||||
Processing language uses for its Catmull-Rom algorithm.</p>
|
||||
|
||||
<p>We start with showing the Catmull-Rom matrix form:</p>
|
||||
|
||||
<p>\[
|
||||
CatmullRom(t) =
|
||||
\begin{bmatrix}
|
||||
1 & t & t^2 & t^3
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
0 & 0 & 1 & 0 \\
|
||||
-3 & 3 & -2 & -1 \\
|
||||
2 & -2 & 1 & 1
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
V_1 \\ V_2 \\ V'_1 \\ V'_2
|
||||
\end{bmatrix}
|
||||
\]
|
||||
</p>
|
||||
|
||||
<p>However, there's something funny going on here: the coordinate column matrix looks weird.
|
||||
The reason is that Catmull-Rom curves are actually curve segments that are described by two
|
||||
points, and two tangents; the curve leaves a point V1 (if we have four coordinates instead,
|
||||
this is coordinate 2), arriving at a point V2 (coordinate 3), with the curve departing V1
|
||||
with a tangent vector V'1 (equal to the tangent from coordinate 1 to coordinate 3) and
|
||||
arriving at V2 with tangent vector V'2 (equal to the tangent from coordinate 2 to coordinate
|
||||
4). So if we want to express this as a matrix form based on four coordinates, we get this
|
||||
representation instead:</p>
|
||||
|
||||
<p>\[
|
||||
\begin{bmatrix}
|
||||
V_1 \\ V_2 \\ V'_1 \\ V'_2
|
||||
\end{bmatrix}
|
||||
=
|
||||
T
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
P_1 \\ P_2 \\ P_3 \\ P_4
|
||||
\end{bmatrix}
|
||||
=
|
||||
\begin{bmatrix}
|
||||
P_2 \\ P_3 \\ \frac{P_3 - P_1}{2} \\ \frac{P_4 - P_2}{2}
|
||||
\end{bmatrix}
|
||||
\ \Rightarrow \
|
||||
T
|
||||
=
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
0 & 0 & 2 & 0 \\
|
||||
-1 & 0 & 1 & 0 \\
|
||||
0 & -1 & 0 & 1
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<div className="note">
|
||||
<h2>Where did that 2 come from?</h2>
|
||||
|
||||
<p>Catmull-Rom splines are based on the concept of tension: the higher the tensions,
|
||||
the shorter the tangents at the departure and arrival points. The basic Catmull-Rom
|
||||
curve arrives and departs with tangents equal to half the distance between the two
|
||||
adjacent points, so that's where that 2 came from.</p>
|
||||
|
||||
<p>However, the "real" matrix is this:</p>
|
||||
|
||||
<p>\[
|
||||
T
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
P_1 \\ P_2 \\ P_3 \\ P_4
|
||||
\end{bmatrix}
|
||||
=
|
||||
\begin{bmatrix}
|
||||
P_2 \\ P_3 \\ \frac{P_3 - P_1}{2 \cdot τ} \\ \frac{P_4 - P_2}{2 \cdot τ}
|
||||
\end{bmatrix}
|
||||
\Rightarrow \
|
||||
T
|
||||
=
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
0 & 0 & 2 & 0 \\
|
||||
-τ & 0 & τ & 0 \\
|
||||
0 & -τ & 0 & τ
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>This bakes in the tension factor τ explicitly.</p>
|
||||
</div>
|
||||
|
||||
<p>Plugging this into the "two coordinates and two tangent vectors" matrix form,
|
||||
we get:</p>
|
||||
|
||||
<p>\[
|
||||
\begin{bmatrix}
|
||||
1 & t & t^2 & t^3
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
0 & 0 & 1 & 0 \\
|
||||
-3 & 3 & -2 & -1 \\
|
||||
2 & -2 & 1 & 1
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
V_1 \\ V_2 \\ V'_1 \\ V'_2
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>\[
|
||||
=
|
||||
\begin{bmatrix}
|
||||
1 & t & t^2 & t^3
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
0 & 0 & 1 & 0 \\
|
||||
-3 & 3 & -2 & -1 \\
|
||||
2 & -2 & 1 & 1
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\left (
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
0 & 0 & 2 & 0 \\
|
||||
-τ & 0 & τ & 0 \\
|
||||
0 & -τ & 0 & τ
|
||||
\end{bmatrix}
|
||||
\right )
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
P_1 \\ P_2 \\ P_3 \\ P_4
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>\[
|
||||
=
|
||||
\begin{bmatrix}
|
||||
1 & t & t^2 & t^3
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
-τ & 0 & τ & 0 \\
|
||||
2τ & τ-6 & -2(τ-3) & -τ \\
|
||||
-τ & 4-τ & τ-4 & τ
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
P_1 \\ P_2 \\ P_3 \\ P_4
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>So let's find out which transformation matrix we need in order to convert from Catmull-Rom to Bézier:</p>
|
||||
|
||||
<p>\[
|
||||
\begin{bmatrix}
|
||||
1 & t & t^2 & t^3
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
-τ & 0 & τ & 0 \\
|
||||
2τ & τ-6 & -2(τ-3) & -τ \\
|
||||
-τ & 4-τ & τ-4 & τ
|
||||
\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 \\
|
||||
-3 & 3 & 0 & 0 \\
|
||||
3 & -6 & 3 & 0 \\
|
||||
-1 & 3 & -3 & 1
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
A
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
P_1 \\ P_2 \\ P_3 \\ P_4
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>The difference is somewhere in the actual hermite matrix, since the <em>t</em> and coordinate
|
||||
values are identical, so let's solve that matrix equasion:</p>
|
||||
|
||||
<p>\[
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
-τ & 0 & τ & 0 \\
|
||||
2τ & τ-6 & -2(τ-3) & -τ \\
|
||||
-τ & 4-τ & τ-4 & τ
|
||||
\end{bmatrix}
|
||||
=
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
-3 & 3 & 0 & 0 \\
|
||||
3 & -6 & 3 & 0 \\
|
||||
-1 & 3 & -3 & 1
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
A
|
||||
\]</p>
|
||||
|
||||
<p>We left-multiply both sides by the inverse of the Bézier matrix, to get rid of the
|
||||
Bézier matrix on the right side of the equals sign:</p>
|
||||
|
||||
<p>\[
|
||||
{
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
-3 & 3 & 0 & 0 \\
|
||||
3 & -6 & 3 & 0 \\
|
||||
-1 & 3 & -3 & 1
|
||||
\end{bmatrix}
|
||||
}^{-1}
|
||||
\cdot
|
||||
\frac{1}{2}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 2 & 0 & 0 \\
|
||||
-τ & 0 & τ & 0 \\
|
||||
2τ & τ-6 & -2(τ-3) & -τ \\
|
||||
-τ & 4-τ & τ-4 & τ
|
||||
\end{bmatrix}
|
||||
=
|
||||
{
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
-3 & 3 & 0 & 0 \\
|
||||
3 & -6 & 3 & 0 \\
|
||||
-1 & 3 & -3 & 1
|
||||
\end{bmatrix}
|
||||
}^{-1}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
1 & 0 & 0 & 0 \\
|
||||
-3 & 3 & 0 & 0 \\
|
||||
3 & -6 & 3 & 0 \\
|
||||
-1 & 3 & -3 & 1
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
A
|
||||
\ =\
|
||||
I \cdot A
|
||||
\ =\
|
||||
A
|
||||
\]
|
||||
</p>
|
||||
|
||||
<p>Which gives us:</p>
|
||||
|
||||
<p>\[
|
||||
\frac{1}{6}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 6 & 0 & 0 \\
|
||||
-τ & 6 & τ & 0 \\
|
||||
0 & τ & 0 & -τ \\
|
||||
0 & 0 & 6 & 0
|
||||
\end{bmatrix}
|
||||
=
|
||||
A
|
||||
\]</p>
|
||||
|
||||
<p>Multiplying this <strong><em>A</em></strong> with our coordinates
|
||||
will give us a proper Bézier matrix expression again:</p>
|
||||
|
||||
<p>\[
|
||||
\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
|
||||
\frac{1}{6}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
0 & 6 & 0 & 0 \\
|
||||
-τ & 6 & τ & 0 \\
|
||||
0 & τ & 0 & -τ \\
|
||||
0 & 0 & 6 & 0
|
||||
\end{bmatrix}
|
||||
\cdot
|
||||
\begin{bmatrix}
|
||||
P_1 \\ P_2 \\ P_3 \\ P_4
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>\[
|
||||
=
|
||||
\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_2 \\
|
||||
P_2 + \frac{P_3-P_1}{6 \cdot τ} \\
|
||||
P_3 - \frac{P_4-P_2}{6 \cdot τ} \\
|
||||
P_3
|
||||
\end{bmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>So a Catmull-Rom to Bézier conversion, based on coordinates, requires turning
|
||||
the Catmull-Rom coordinates on the left into the Bézier coordinates on the right
|
||||
(with τ being our tension factor):</p>
|
||||
|
||||
<p>\[
|
||||
\begin{bmatrix}
|
||||
P_1 \\
|
||||
P_2 \\
|
||||
P_3 \\
|
||||
P_4
|
||||
\end{bmatrix}_{CatmullRom}
|
||||
\Rightarrow
|
||||
\begin{bmatrix}
|
||||
P_2 \\
|
||||
P_2 + \frac{P_3-P_1}{6 \cdot τ} \\
|
||||
P_3 - \frac{P_4-P_2}{6 \cdot τ} \\
|
||||
P_3
|
||||
\end{bmatrix}_{Bézier}
|
||||
\]</p>
|
||||
|
||||
<p>And the other way around, a Bézier to Catmull-Rom conversion requires turning
|
||||
the Bézier coordinates on the left this time into the Catmull-Rom coordinates
|
||||
on the right. Note that there is no tension this time, because Bézier curves
|
||||
don't have any. Converting from Bézier to Catmull-Rom is simply a default-tension
|
||||
Catmull-Rom curve:</p>
|
||||
|
||||
<p>\[
|
||||
\begin{bmatrix}
|
||||
P_1 \\
|
||||
P_2 \\
|
||||
P_3 \\
|
||||
P_4
|
||||
\end{bmatrix}_{Bézier}
|
||||
\Rightarrow
|
||||
\begin{bmatrix}
|
||||
P_4 + 6 \cdot (P_1 - P_2) \\
|
||||
P_1 \\
|
||||
P_4 \\
|
||||
P_1 + 6 \cdot (P_4 - P_3)
|
||||
\end{bmatrix}_{CatmullRom}
|
||||
\]</p>
|
||||
|
||||
<p>Done. We can now draw the curves we want using either Bézier curves or Catmull-Rom
|
||||
splines, the choice mostly being which drawing algorithms we have natively available.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CatmullRomConversion;
|
184
components/sections/catmullmoulding/index.js
Normal file
184
components/sections/catmullmoulding/index.js
Normal file
@@ -0,0 +1,184 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var CatmullRomMoulding = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Creating a Catmull-Rom curve from three points"
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api) {
|
||||
api.setPanelCount(3);
|
||||
api.lpts = [
|
||||
{x:56, y:153},
|
||||
{x:144,y:83},
|
||||
{x:188,y:185}
|
||||
];
|
||||
api.distance = 0;
|
||||
},
|
||||
|
||||
onKeyDown: function(evt, api) {
|
||||
if (evt.key === "ArrowUp") {
|
||||
evt.preventDefault();
|
||||
api.distance += 1;
|
||||
} else if (evt.key === "ArrowDown") {
|
||||
evt.preventDefault();
|
||||
api.distance -= 1;
|
||||
}
|
||||
},
|
||||
|
||||
convert: function(p1, p2, p3, p4) {
|
||||
var t = 0.5;
|
||||
return [
|
||||
p2,
|
||||
{
|
||||
x: p2.x + (p3.x-p1.x)/(6*t),
|
||||
y: p2.y + (p3.y-p1.y)/(6*t),
|
||||
},
|
||||
{
|
||||
x: p3.x - (p4.x-p2.x)/(6*t),
|
||||
y: p3.y - (p4.y-p2.y)/(6*t),
|
||||
},
|
||||
p3
|
||||
];
|
||||
},
|
||||
|
||||
draw: function(api) {
|
||||
api.reset();
|
||||
api.setColor("lightblue");
|
||||
api.drawGrid(10,10);
|
||||
|
||||
var pts = api.lpts;
|
||||
api.setColor("black");
|
||||
api.setFill("black");
|
||||
pts.forEach((p,pos) => {
|
||||
api.drawCircle(p, 3);
|
||||
api.text("point "+(pos+1), p, {x:10, y:7});
|
||||
});
|
||||
|
||||
var w = api.getPanelWidth();
|
||||
var h = api.getPanelHeight();
|
||||
var offset = {x:w, y:0};
|
||||
api.setColor("lightblue");
|
||||
api.drawGrid(10,10,offset);
|
||||
api.setColor("black");
|
||||
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
|
||||
|
||||
pts.forEach((p,pos) => {
|
||||
api.drawCircle(p, 3, offset);
|
||||
});
|
||||
var p1 = pts[0], p2 = pts[1], p3 = pts[2];
|
||||
var dx = p3.x - p1.x,
|
||||
dy = p3.y - p1.y,
|
||||
m = Math.sqrt(dx*dx + dy*dy);
|
||||
dx /= m;
|
||||
dy /= m;
|
||||
api.drawLine(p1, p3, offset);
|
||||
|
||||
var p0 = {
|
||||
x: p1.x + (p3.x - p2.x) - api.distance * dx,
|
||||
y: p1.y + (p3.y - p2.y) - api.distance * dy
|
||||
};
|
||||
var p4 = {
|
||||
x: p1.x + (p3.x - p2.x) + api.distance * dx,
|
||||
y: p1.y + (p3.y - p2.y) + api.distance * dy
|
||||
};
|
||||
var center = api.utils.lli4(p1,p3,p2,{
|
||||
x: (p0.x + p4.x)/2,
|
||||
y: (p0.y + p4.y)/2
|
||||
});
|
||||
api.setColor("blue");
|
||||
api.drawCircle(center, 3, offset);
|
||||
api.drawLine(pts[1],center, offset);
|
||||
api.setColor("#666");
|
||||
api.drawLine(center, p0, offset);
|
||||
api.drawLine(center, p4, offset);
|
||||
|
||||
api.setFill("blue");
|
||||
api.text("p0", p0, {x:-20 + offset.x, y:offset.y + 2});
|
||||
api.text("p4", p4, {x:+10 + offset.x, y:offset.y + 2});
|
||||
|
||||
// virtual point p0
|
||||
api.setColor("red");
|
||||
api.drawCircle(p0, 3, offset);
|
||||
api.drawLine(p2, p0, offset);
|
||||
api.drawLine(p1, {
|
||||
x: p1.x + (p2.x - p0.x)/5,
|
||||
y: p1.y + (p2.y - p0.y)/5
|
||||
}, offset);
|
||||
|
||||
// virtual point p4
|
||||
api.setColor("#00FF00");
|
||||
api.drawCircle(p4, 3, offset);
|
||||
api.drawLine(p2, p4, offset);
|
||||
api.drawLine(p3, {
|
||||
x: p3.x + (p4.x - p2.x)/5,
|
||||
y: p3.y + (p4.y - p2.y)/5
|
||||
}, offset);
|
||||
|
||||
// Catmull-Rom curve for p0-p1-p2-p3-p4
|
||||
var c1 = new api.Bezier(this.convert(p0,p1,p2,p3)),
|
||||
c2 = new api.Bezier(this.convert(p1,p2,p3,p4));
|
||||
api.setColor("lightgrey");
|
||||
api.drawCurve(c1, offset);
|
||||
api.drawCurve(c2, offset);
|
||||
|
||||
|
||||
offset.x += w;
|
||||
api.setColor("lightblue");
|
||||
api.drawGrid(10,10,offset);
|
||||
api.setColor("black");
|
||||
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
|
||||
|
||||
api.drawCurve(c1, offset);
|
||||
api.drawCurve(c2, offset);
|
||||
api.drawPoints(c1.points, offset);
|
||||
api.drawPoints(c2.points, offset);
|
||||
api.setColor("lightgrey");
|
||||
api.drawLine(c1.points[0], c1.points[1], offset);
|
||||
api.drawLine(c1.points[2], c2.points[1], offset);
|
||||
api.drawLine(c2.points[2], c2.points[3], offset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Now, we saw how to fit a Bézier curve to three points, but if Catmull-Rom curves go through points,
|
||||
why can't we just use those to do curve fitting, instead?</p>
|
||||
|
||||
<p>As a matter of fact, we can, but there's a difference between the kind of curve fitting we did in the
|
||||
previous section, and the kind of curve fitting that we can do with Catmull-Rom curves. In the previous
|
||||
section we came up with a single curve that goes through three points. There was a decent amount of
|
||||
maths and computation involved, and the end result was three or four coordinates that described a
|
||||
single curve, depending on whether we were fitting a quadratic or cubic curve.</p>
|
||||
|
||||
<p>Using Catmull-Rom curves, we need virtually no computation, but even though we end up with one Catmull-Rom
|
||||
curve of <i>n</i> points, in order to draw the equivalent curve using cubic Bézier curves we need a
|
||||
massive <i>3n-1</i> points (and that's without double-counting points that are shared by consecutive
|
||||
cubic curves).</p>
|
||||
|
||||
|
||||
<p>In the following graphic, on the left we see three points that we want to draw a Catmull-Rom curve through
|
||||
(which we can move around freely, by the way), with in the second panel some of the "interesting" Catmull-Rom
|
||||
information: in black there's the baseline start--end, which will act as tangent orientation for the curve at
|
||||
point p2. We also see a virtual point p0 and p4, which are initially just point p2 reflected over the baseline.
|
||||
However, by using the up and down cursor key we can offset these points parallel to the baseline. Why would
|
||||
we want to do this? Because the line p0--p2 acts as departure tangent at p1, and the line p2--p4 acts as
|
||||
arrival tangent at p3. Play around with the graphic a bit to get an idea of what all of that meant:</p>
|
||||
|
||||
<Graphic preset="threepanel" title="Catmull-Rom curve fitting" setup={this.setup} draw={this.draw} onKeyDown={this.onKeyDown}/>
|
||||
|
||||
<p>As should be obvious by now, Catmull-Rom curves are great for "fitting a curvature to some points", but if we want
|
||||
to convert that curve to Bézier form we're going to end up with a lot of separate (but visually joined) Bézier curves.
|
||||
Depending on what we want to do, that'll be either unnecessary work, or exactly what we want: which it is depends
|
||||
entirely on you.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CatmullRomMoulding;
|
@@ -34,14 +34,14 @@ module.exports = {
|
||||
|
||||
abc: require("./abc"),
|
||||
moulding: require("./moulding"),
|
||||
pointcurves: require("./pointcurves")
|
||||
pointcurves: require("./pointcurves"),
|
||||
|
||||
catmullconv: require("./catmullconv"),
|
||||
catmullmoulding: require("./catmullmoulding")
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
catmullconv: require("./catmullconv"),
|
||||
catmullmoulding: require("./catmullmoulding"),
|
||||
|
||||
polybezier: require("./polybezier"),
|
||||
shapes: require("./shapes"),
|
||||
|
||||
@@ -56,9 +56,6 @@ module.exports = {
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
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
|
||||
|
@@ -234,7 +234,7 @@ var Moulding = React.createClass({
|
||||
we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:</p>
|
||||
|
||||
<p>\[
|
||||
A' = B' + \frac{B' - C}{ratio} = B' - \frac{C - B'}{ratio}
|
||||
A' = B' - \frac{C - B'}{ratio} = B' + \frac{B - C}{ratio}
|
||||
\]</p>
|
||||
|
||||
<p>For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point.
|
||||
@@ -249,7 +249,9 @@ var Moulding = React.createClass({
|
||||
curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points
|
||||
should be. Remember that B' lies on line e1--e2 at a distance <i>t</i>, because that's how Bézier curves work. In the same manner,
|
||||
we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing
|
||||
the new control points is fairly easy:</p>
|
||||
the new control points is fairly easy.</p>
|
||||
|
||||
<p>First, we construct the one-level-of-de-Casteljau-up points:</p>
|
||||
|
||||
<p>\[
|
||||
\left \{ \begin{align}
|
||||
@@ -258,6 +260,8 @@ var Moulding = React.createClass({
|
||||
\end{align} \right .
|
||||
\]</p>
|
||||
|
||||
<p>And then we can compute the new control points:</p>
|
||||
|
||||
<p>\[
|
||||
\left \{ \begin{align}
|
||||
C1' &= v1 + \frac{v1 - start}{t} \\
|
||||
|
@@ -12,7 +12,11 @@ var PointCurves = React.createClass({
|
||||
},
|
||||
|
||||
setup: function(api) {
|
||||
api.lpts = [];
|
||||
api.lpts = [
|
||||
{x:56, y:153},
|
||||
{x:144,y:83},
|
||||
{x:188,y:185}
|
||||
];
|
||||
},
|
||||
|
||||
onClick: function(evt, api) {
|
||||
@@ -209,8 +213,8 @@ var PointCurves = React.createClass({
|
||||
<p>In each graphic, the blue parts are the values that we "just have" simply by setting up
|
||||
our three points, combined with our decision on which <i>t</i> value to use (and construction line
|
||||
orientation and length for cubic curves). There are of course many ways to determine a combination
|
||||
of <i>t</i> and tangent values that lead to a more "aesthetic" curve, but this will be left as an
|
||||
exercise to the reader, since there are many, and aesthetics are often quite personal.</p>
|
||||
of <i>t</i> and tangent values that lead to a more "æsthetic" curve, but this will be left as an
|
||||
exercise to the reader, since there are many, and æsthetics are often quite personal.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user