mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-30 03:30:34 +02:00
two sections left
This commit is contained in:
245
components/sections/arcapproximation/index.js
Normal file
245
components/sections/arcapproximation/index.js
Normal file
@@ -0,0 +1,245 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var atan2 = Math.atan2, PI = Math.PI, TAU = 2*PI, cos = Math.cos, sin = Math.sin;
|
||||
|
||||
var Introduction = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Approximating Bézier curves with circular arcs"
|
||||
};
|
||||
},
|
||||
|
||||
setupCircle: function(api) {
|
||||
var curve = new api.Bezier(70,70, 140,40, 240,130);
|
||||
api.setCurve(curve);
|
||||
},
|
||||
|
||||
setupQuadratic: function(api) {
|
||||
var curve = api.getDefaultQuadratic();
|
||||
api.setCurve(curve);
|
||||
},
|
||||
|
||||
setupCubic: function(api) {
|
||||
var curve = api.getDefaultCubic();
|
||||
api.setCurve(curve);
|
||||
},
|
||||
|
||||
getCCenter: function(api, p1, p2, p3) {
|
||||
// deltas
|
||||
var dx1 = (p2.x - p1.x),
|
||||
dy1 = (p2.y - p1.y),
|
||||
dx2 = (p3.x - p2.x),
|
||||
dy2 = (p3.y - p2.y);
|
||||
|
||||
// perpendiculars (quarter circle turned)
|
||||
var dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2),
|
||||
dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2),
|
||||
dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2),
|
||||
dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2);
|
||||
|
||||
// chord midpoints
|
||||
var mx1 = (p1.x + p2.x)/2,
|
||||
my1 = (p1.y + p2.y)/2,
|
||||
mx2 = (p2.x + p3.x)/2,
|
||||
my2 = (p2.y + p3.y)/2;
|
||||
|
||||
// midpoint offsets
|
||||
var mx1n = mx1 + dx1p,
|
||||
my1n = my1 + dy1p,
|
||||
mx2n = mx2 + dx2p,
|
||||
my2n = my2 + dy2p;
|
||||
|
||||
// intersection of these lines:
|
||||
var i = api.utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n);
|
||||
var r = api.utils.dist(i,p1);
|
||||
|
||||
// arc start/end values, over mid point
|
||||
var s = atan2(p1.y - i.y, p1.x - i.x),
|
||||
m = atan2(p2.y - i.y, p2.x - i.x),
|
||||
e = atan2(p3.y - i.y, p3.x - i.x);
|
||||
|
||||
// determine arc direction (cw/ccw correction)
|
||||
var __;
|
||||
if (s<e) {
|
||||
// if s<m<e, arc(s, e)
|
||||
// if m<s<e, arc(e, s + TAU)
|
||||
// if s<e<m, arc(e, s + TAU)
|
||||
if (s>m || m>e) { s += TAU; }
|
||||
if (s>e) { __=e; e=s; s=__; }
|
||||
} else {
|
||||
// if e<m<s, arc(e, s)
|
||||
// if m<e<s, arc(s, e + TAU)
|
||||
// if e<s<m, arc(s, e + TAU)
|
||||
if (e<m && m<s) { __=e; e=s; s=__; } else { e += TAU; }
|
||||
}
|
||||
|
||||
// assign and done.
|
||||
i.s = s;
|
||||
i.e = e;
|
||||
i.r = r;
|
||||
return i;
|
||||
},
|
||||
|
||||
drawCircle: function(api, curve) {
|
||||
api.reset();
|
||||
var pts = curve.points;
|
||||
|
||||
// get center
|
||||
var C = this.getCCenter(api, pts[0], pts[1], pts[2]);
|
||||
api.setColor("black");
|
||||
pts.forEach(p => api.drawCircle(p,3));
|
||||
api.drawCircle(C, 3);
|
||||
|
||||
// chords and perpendicular lines
|
||||
api.setColor("blue");
|
||||
api.drawLine(pts[0], pts[1]);
|
||||
api.drawLine({x: (pts[0].x + pts[1].x)/2, y: (pts[0].y + pts[1].y)/2}, C);
|
||||
|
||||
api.setColor("red");
|
||||
api.drawLine(pts[1], pts[2]);
|
||||
api.drawLine({x: (pts[1].x + pts[2].x)/2, y: (pts[1].y + pts[2].y)/2}, C);
|
||||
|
||||
api.setColor("green");
|
||||
api.drawLine(pts[2], pts[0]);
|
||||
api.drawLine({x: (pts[2].x + pts[0].x)/2, y: (pts[2].y + pts[0].y)/2}, C);
|
||||
|
||||
api.setColor("grey");
|
||||
api.drawCircle(C, api.utils.dist(C,pts[0]));
|
||||
},
|
||||
|
||||
drawSingleArc: function(api, curve) {
|
||||
api.reset();
|
||||
var arcs = curve.arcs(0.5);
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
|
||||
var a = arcs[0];
|
||||
api.setColor("red");
|
||||
api.setFill("rgba(200,0,0,0.4)");
|
||||
api.debug = true;
|
||||
api.drawArc(a);
|
||||
},
|
||||
|
||||
drawArcs: function(api, curve) {
|
||||
api.reset();
|
||||
var arcs = curve.arcs(0.5);
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
arcs.forEach(a => {
|
||||
api.setRandomColor(0.3);
|
||||
api.setFill(api.getColor());
|
||||
api.drawArc(a);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Let's look at converting Bézier curves into sequences of circular arcs. We already saw in the
|
||||
section on circle approximation that this will never yield a perfect equivalent, but sometimes
|
||||
you need circular arcs, such as when you're working with fabrication machinery, or simple vector
|
||||
languages that understand lines and circles, but not much else.</p>
|
||||
|
||||
<p>The approach is fairly simple: pick a starting point on the curve, and pick two points that are
|
||||
further along the curve. Determine the circle that goes through those three points, and see if
|
||||
it fits the part of the curve we're trying to approximate. Decent fit? Try spacing the points
|
||||
further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've
|
||||
found the "good approximation/bad approximation" boundary, record the "good" arc, and then move
|
||||
the starting point up to overlap the end point we previously found. Rinse and repeat until we've
|
||||
covered the entire curve.</p>
|
||||
|
||||
<p>So: step 1, how do we find a circle through three points? That part is actually really simple.
|
||||
You may remember (if you ever learned it!) that a line between two points on a circle is called
|
||||
a <a href="https://en.wikipedia.org/wiki/Chord_%28geometry%29">chord</a>, and one property of
|
||||
chords is that the line from the center of any chord, perpendicular to that chord, passes through
|
||||
the center of the circle.</p>
|
||||
|
||||
<p>So: if we have have three points, we have three (different) chords, and consequently, three
|
||||
(different) lines that go from those chords through the center of the circle. So we find the
|
||||
centers of the chords, find the perpendicular lines, find the intersection of those lines,
|
||||
and thus find the center of the circle.</p>
|
||||
|
||||
<p>The following graphic shows this procedure with a different colour for each chord and its
|
||||
associated perpendicular through the center. You can move the points around as much as you
|
||||
like, those lines will always meet!</p>
|
||||
|
||||
<Graphic preset="simple" title="Finding a circle through three points" setup={this.setupCircle} draw={this.drawCircle} />
|
||||
|
||||
<p>So, with the procedure on how to find a circle through three points, finding the arc through those points
|
||||
is straight-forward: pick one of the three points as start point, pick another as an end point, and
|
||||
the arc has to necessarily go from the start point, over the remaining point, to the end point.</p>
|
||||
|
||||
<p>So how can we convert a Bezier curve into a (sequence of) circular arc(s)?</p>
|
||||
|
||||
<ul>
|
||||
<li>Start at <em>t=0</em></li>
|
||||
<li>Pick two points further down the curve at some value <em>m = t + n</em> and <em>e = t + 2n</em></li>
|
||||
<li>Find the arc that these points define</li>
|
||||
<li>Determine how close the found arc is to the curve:
|
||||
<ul>
|
||||
<li>Pick two additional points <em>e1 = t + n/2</em> and <em>e2 = t + n + n/2</em>.</li>
|
||||
<li>These points, if the arc is a good approximation of the curve interval chosen, should
|
||||
lie <em>on</em> the circle, so their distance to the center of the circle should be the
|
||||
same as the distance from any of the three other points to the center.</li>
|
||||
<li>For point points, determine the (absolute) error between the radius of the circle, and the
|
||||
<em>actual</em> distance from the center of the circle to the point on the curve.</li>
|
||||
<li>If this error is too high, we consider the arc bad, and try a smaller interval.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's
|
||||
the entire curve. The midpoint is simply at <em>t=0.5</em>, and then we start performing
|
||||
a <a href="https://en.wikipedia.org/wiki/Binary_search_algorithm">Binary Search</a>.</p>
|
||||
|
||||
<ol>
|
||||
<li>We start with {0, 0.5, 1}</li>
|
||||
<li>That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}</li>
|
||||
<ul>
|
||||
<li>If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.</li>
|
||||
<li>However, if the arc was still bad, we move <em>down</em> by half the distance: {0, 0.125, 0.25}.</li>
|
||||
</ul>
|
||||
<li>We keep doing this over and over until we have two arcs found in sequence of which the first arc is good, and
|
||||
the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a
|
||||
bad approximation, and we pick the former</li>
|
||||
</ol>
|
||||
|
||||
<p>The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that
|
||||
if an arc is off by a <em>combined</em> half pixel over both verification points, then we treat the arc as bad.
|
||||
This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive,
|
||||
and you can use your '+' and '-' keys to increase to decrease the error threshold, to see what the effect
|
||||
of a smaller or larger error threshold is.</p>
|
||||
|
||||
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawSingleArc} />
|
||||
|
||||
<p>With that in place, all that's left now is to "restart" the procedure by treating the found arc's
|
||||
end point as the new to-be-determined arc's starting point, and using points further down the curve. We
|
||||
keep trying this until the found end point is for <em>t=1</em>, at which point we are done. Again,
|
||||
the following graphic allows for '+' and '-' key input to increase or decrease the error threshold,
|
||||
so you can see how picking a different threshold changes the number of arcs that are necessary to
|
||||
reasonably approximate a curve:</p>
|
||||
|
||||
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawArcs} />
|
||||
|
||||
<p>So... what is this good for? Obviously, If you're working with technologies that can't do curves,
|
||||
but can do lines and circles, then the answer is pretty straight-forward, but what else? There are
|
||||
some reasons why you might need this technique: using circular arcs means you can determine whether
|
||||
a coordinate lies "on" your curve really easily: simply compute the distance to each circular arc
|
||||
center, and if any of those are close to the arc radii, at an angle betwee the arc start and end:
|
||||
bingo, this point can be treated as lying "on the curve". Another benefit is that this approximation
|
||||
is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially
|
||||
compute the arc length of the approximated curve (it's a bit like curve flattening). The only
|
||||
thing to bear in mind is that this is a lossy equivalence: things that you compute based on the
|
||||
approximation are guaranteed "off" by some small value, and depending on how much precision you
|
||||
need, arc approximation is either going to be super useful, or completely useless. It's up to you
|
||||
to decide which, based on your application!</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Introduction;
|
246
components/sections/circles/index.js
Normal file
246
components/sections/circles/index.js
Normal file
@@ -0,0 +1,246 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var sin = Math.sin, cos = Math.cos;
|
||||
|
||||
var Circles = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Circles and quadratic Bézier curves"
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api) {
|
||||
api.w = api.getPanelWidth();
|
||||
api.h = api.getPanelHeight();
|
||||
api.pad = 20;
|
||||
api.r = api.w/2 - api.pad;
|
||||
api.mousePt = false;
|
||||
api.angle = 0;
|
||||
var spt = { x: api.w-api.pad, y: api.h/2 };
|
||||
api.setCurve(new api.Bezier(spt, spt, spt));
|
||||
},
|
||||
|
||||
draw: function(api, curve) {
|
||||
api.reset();
|
||||
api.setColor("lightgrey");
|
||||
api.drawGrid(1,1);
|
||||
api.setColor("red");
|
||||
api.drawCircle({x:api.w/2,y:api.h/2},api.r);
|
||||
api.setColor("transparent");
|
||||
api.setFill("rgba(100,255,100,0.4)");
|
||||
var p = {
|
||||
x: api.w/2,
|
||||
y: api.h/2,
|
||||
r: api.r,
|
||||
s: api.angle < 0 ? api.angle : 0,
|
||||
e: api.angle < 0 ? 0 : api.angle,
|
||||
};
|
||||
api.drawArc(p);
|
||||
api.setColor("black");
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
},
|
||||
|
||||
onMouseMove: function(evt, api) {
|
||||
var x = evt.offsetX - api.w/2,
|
||||
y = evt.offsetY - api.h/2;
|
||||
var angle = Math.atan2(y,x);
|
||||
var pts = api.curve.points;
|
||||
// new control
|
||||
var r = api.r,
|
||||
b = (cos(angle) - 1) / sin(angle);
|
||||
pts[1] = {
|
||||
x: api.w/2 + r * (cos(angle) - b * sin(angle)),
|
||||
y: api.w/2 + r * (sin(angle) + b * cos(angle))
|
||||
};
|
||||
// new endpoint
|
||||
pts[2] = {
|
||||
x: api.w/2 + api.r * cos(angle),
|
||||
y: api.w/2 + api.r * sin(angle)
|
||||
};
|
||||
api.setCurve(new api.Bezier(pts));
|
||||
api.angle = angle;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Circles and Bézier curves are very different beasts, and circles are infinitely easier
|
||||
to work with than Bézier curves. Their formula is much simpler, and they can be drawn more
|
||||
efficiently. But, sometimes you don't have the luxury of using circles, or ellipses, or
|
||||
arcs. Sometimes, all you have are Bézier curves. For instance, if you're doing font design,
|
||||
fonts have no concept of geometric shapes, they only know straight lines, and Bézier curves.
|
||||
OpenType fonts with TrueType outlines only know quadratic Bézier curves, and OpenType fonts
|
||||
with Type 2 outlines only know cubic Bézier curves. So how do you draw a circle, or an ellipse,
|
||||
or an arc?</p>
|
||||
|
||||
<p>You approximate.</p>
|
||||
|
||||
<p>We already know that Bézier curves cannot model all curves that we can think of, and
|
||||
this includes perfect circles, as well as ellipses, and their arc counterparts. However,
|
||||
we can certainly approximate them to a degree that is visually acceptable. Quadratic and cubic
|
||||
curves offer us different curvature control, so in order to approximate a circle we will
|
||||
first need to figure out what the error is if we try to approximate arcs of increasing degree
|
||||
with quadratic and cubic curves, and where the coordinates even lie.</p>
|
||||
|
||||
<p>Since arcs are mid-point-symmetrical, we need the control points to set up a symmetrical
|
||||
curve. For quadratic curves this means that the control point will be somewhere on a line
|
||||
that intersects the baseline at a right angle. And we don't get any choice on where that
|
||||
will be, since the derivatives at the start and end point have to line up, so our control
|
||||
point will lie at the intersection of the tangents at the start and end point.</p>
|
||||
|
||||
<p>First, let's try to fit the quadratic curve onto a circular arc. In the following sketch
|
||||
you can move the mouse around over a unit circle, to see how well, or poorly, a quadratic
|
||||
curve can approximate the arc from (1,0) to where your mouse cursor is:</p>
|
||||
|
||||
<Graphic preset="arcfitting" title="Quadratic Bézier arc approximation" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
|
||||
<p>As you can see, things go horribly wrong quite quickly; even trying to approximate a quarter circle
|
||||
using a quadratic curve is a bad idea. An eighth of a turns might look okay, but how okay is okay?
|
||||
Let's apply some maths and find out. What we're interested in is how far off our on-curve coordinates
|
||||
are with respect to a circular arc, given a specific start and end angle. We'll be looking at how much
|
||||
space there is between the circular arc, and the quadratic curve's midpoint.</p>
|
||||
|
||||
<p>We start out with our start and end point, and for convenience we will place them on a unit
|
||||
circle (a circle around 0,0 with radius 1), at some angle <i>φ</i>:</p>
|
||||
|
||||
<p>\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]</p>
|
||||
|
||||
<p>What we want to find is the intersection of the tangents, so we want a point C such that:</p>
|
||||
|
||||
<p>\[ C = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix} \ , \ \ C = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix} \]</p>
|
||||
|
||||
<p>i.e. we want a point that lies on the vertical line through A (at some distance <i>a</i>
|
||||
from A) and also lies on the tangent line through B (at some distance <i>b</i> from B). Solving
|
||||
this gives us:</p>
|
||||
|
||||
<p>\[ \left\{ \begin{array}{l}
|
||||
C_x = 1 = cos(φ) + b \cdot -sin(φ)\\
|
||||
C_y = a = sin(φ) + b \cdot cos(φ)
|
||||
\end{array} \right. \]</p>
|
||||
|
||||
<p>First we solve for <i>b</i>:</p>
|
||||
|
||||
<p>\[ \begin{array}{l}
|
||||
1 = cos(φ) + b \cdot -sin(φ) \ → \
|
||||
1 - cos(φ) = -b \cdot sin(φ) \ → \
|
||||
-1 + cos(φ) = b \cdot sin(φ)
|
||||
\end{array} \]</p>
|
||||
|
||||
<p>which yields:</p>
|
||||
|
||||
<p>\[
|
||||
b = \frac{cos(φ)-1}{sin(φ)}
|
||||
\]</p>
|
||||
|
||||
<p>which we can then substitute in the expression for <i>a</i>:</p>
|
||||
|
||||
<p>\[ \begin{align*}
|
||||
a &= sin(φ) + b \cdot cos(φ) \\
|
||||
.. &= sin(φ) + \frac{-1 + cos(φ)}{sin(φ)} \cdot cos(φ) \\
|
||||
.. &= sin(φ) + \frac{-cos(φ) + cos^2(φ)}{sin(φ)} \\
|
||||
.. &= \frac{sin^2(φ) + cos^2(φ) - cos(φ)}{sin(φ)} \\
|
||||
a &= \frac{1 - cos(φ)}{sin(φ)}
|
||||
\end{align*} \]</p>
|
||||
|
||||
<p>A quick check shows that plugging these values for <i>a</i> and <i>b</i> into the expressions
|
||||
for C<sub>x</sub> and C<sub>y</sub> give the same x/y coordinates for both "<i>a</i> away from A"
|
||||
and "<i>b</i> away from B", so let's continue: now that we know the coordinate values for C, we
|
||||
know where our on-curve point T for <i>t=0.5</i> (or angle φ/2) is, because we can just evaluate
|
||||
the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:</p>
|
||||
|
||||
<p>\[
|
||||
P_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
|
||||
\]</p>
|
||||
|
||||
<p>We compute T, observing that if <i>t=0.5</i>, the polynomial values (1-t)², 2(1-t)t, and t²
|
||||
are 0.25, 0.5, and 0.25 respectively:</p>
|
||||
|
||||
<p>\[
|
||||
T = \frac{1}{4}S + \frac{2}{4}C + \frac{1}{4}E = \frac{1}{4}(S + 2C + E)
|
||||
\]</p>
|
||||
|
||||
<p>Which, worked out for the x and y components, gives:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
\left\{\begin{align*}
|
||||
T_x &= \frac{1}{4}(3 + cos(φ))\\
|
||||
T_y &= \frac{1}{4}\left(\frac{2-2cos(φ)}{sin(φ)} + sin(φ)\right)
|
||||
= \frac{1}{4}\left(2tan\left(\frac{φ}{2}\right) + sin(φ)\right)
|
||||
\end{align*}\right.
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>And the distance between these two is the standard Euclidean distance:</p>
|
||||
|
||||
<p>\[\begin{align}
|
||||
d_x(φ) &= T_x - P_x = \frac{1}{4}(3 + cos(φ)) - cos(\frac{φ}{2}) = 2sin^4\left(\frac{φ}{4}\right) \ , \\
|
||||
d_y(φ) &= T_y - P_y = \frac{1}{4}\left(2tan\left(\frac{φ}{2}\right) + sin(φ)\right) - sin(\frac{φ}{2}) \ , \\
|
||||
&⇓\\
|
||||
d(φ) &= \sqrt{d^2_x + d^2_y} = \ ... \ = 2sin^4(\frac{φ}{2})\sqrt{\frac{1}{cos^2(\frac{φ}{2})}}
|
||||
\end{align}\]</p>
|
||||
|
||||
<p>So, what does this distance function look like when we plot it for a
|
||||
number of ranges for the angle φ, such as a half circle, quarter circle and eighth circle?</p>
|
||||
|
||||
<table><tbody><tr><td>
|
||||
<p>
|
||||
<a href="http://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi">
|
||||
<img src="images/arc-q-pi.gif"/>
|
||||
</a>
|
||||
</p>
|
||||
<p>plotted for 0 ≤ φ ≤ π:</p>
|
||||
</td><td>
|
||||
<p>
|
||||
<a href="http://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi%2F2">
|
||||
<img src="images/arc-q-pi2.gif"/>
|
||||
</a>
|
||||
</p>
|
||||
<p>plotted for 0 ≤ φ ≤ ½π:</p>
|
||||
</td><td>
|
||||
<p>
|
||||
<a href="http://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi%2F4">
|
||||
<img src="images/arc-q-pi4.gif"/>
|
||||
</a>
|
||||
</p>
|
||||
<p>plotted for 0 ≤ φ ≤ ¼π:</p>
|
||||
</td></tr></tbody></table>
|
||||
|
||||
<p>We now see why the eighth circle arc looks decent, but the quarter circle arc doesn't:
|
||||
an error of roughly 0.06 at <i>t=0.5</i> means we're 6% off the mark... we will already be
|
||||
off by one pixel on a circle with pixel radius 17. Any decent sized quarter circle arc, say
|
||||
with radius 100px, will be way off if approximated by a quadratic curve! For the eighth
|
||||
circle arc, however, the error is only roughly 0.003, or 0.3%, which explains why it looks
|
||||
so close to the actual eighth circle arc. In fact, if we want a truly tiny error, like 0.001,
|
||||
we'll have to contend with an angle of (rounded) 0.593667, which equates to roughly 34 degrees.
|
||||
We'd need 11 quadratic curves to form a full circle with that precision! (technically,
|
||||
10 and ten seventeenth, but we can't do partial curves, so we have to round up). That's a
|
||||
whole lot of curves just to get a shape that can be drawn using a simple function!</p>
|
||||
|
||||
<p>In fact, let's flip the function around, so that if we plug in the precision error, labelled
|
||||
ε, we get back the maximum angle for that precision:</p>
|
||||
|
||||
<p>\[
|
||||
φ = 4 \cdot arccos \left(\frac{\sqrt{2+ε-\sqrt{ε(2+ε)}}}{\sqrt{2}}\right)
|
||||
\]</p>
|
||||
|
||||
<p>And frankly, things are starting to look a bit ridiculous at this point, we're doing way more
|
||||
maths than we've ever done, but thankfully this is as far as we need the maths to take us:
|
||||
If we plug in the precisions 0.1, 0.01, 0.001 and 0.0001 we get the radians values 1.748, 1.038, 0.594
|
||||
and 0.3356; in degrees, that means we can cover roughly 100 degrees (requiring four curves),
|
||||
59.5 degrees (requiring six curves), 34 degrees (requiring 11 curves), and 19.2 degrees (requiring
|
||||
a whopping nineteen curves). </p>
|
||||
|
||||
<p>The bottom line? <strong>Quadratic curves are kind of lousy</strong> if you want circular
|
||||
(or elliptical, which are circles that have been squashed in one dimension) curves. We
|
||||
can do better, even if it's just by raising the order of our curve once. So let's try the
|
||||
same thing for cubic curves.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Circles;
|
444
components/sections/circles_cubic/index.js
Normal file
444
components/sections/circles_cubic/index.js
Normal file
@@ -0,0 +1,444 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var sin = Math.sin, cos = Math.cos, tan = Math.tan, abs = Math.abs;
|
||||
|
||||
var CirclesCubic = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Circles and cubic Bézier curves"
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api) {
|
||||
api.setSize(400,400);
|
||||
api.w = api.getPanelWidth();
|
||||
api.h = api.getPanelHeight();
|
||||
api.pad = 80;
|
||||
api.r = api.w/2 - api.pad;
|
||||
api.mousePt = false;
|
||||
api.angle = 0;
|
||||
var spt = { x: api.w-api.pad, y: api.h/2 };
|
||||
api.setCurve(new api.Bezier(spt, spt, spt, spt));
|
||||
},
|
||||
|
||||
guessCurve: function(S, B, E) {
|
||||
var C = {
|
||||
x: (S.x + E.x)/2,
|
||||
y: (S.y + E.y)/2
|
||||
},
|
||||
A = {
|
||||
x: B.x + (B.x-C.x)/3, // cubic ratio at t=0.5 is 1/3
|
||||
y: B.y + (B.y-C.y)/3
|
||||
},
|
||||
bx = (E.x-S.x)/4,
|
||||
by = (E.y-S.y)/4,
|
||||
e1 = {
|
||||
x: B.x - bx,
|
||||
y: B.y - by
|
||||
},
|
||||
e2 = {
|
||||
x: B.x + bx,
|
||||
y: B.y + by
|
||||
},
|
||||
|
||||
v1 = {
|
||||
x: A.x + (e1.x-A.x)*2,
|
||||
y: A.y + (e1.y-A.y)*2
|
||||
},
|
||||
v2 = {
|
||||
x: A.x + (e2.x-A.x)*2,
|
||||
y: A.y + (e2.y-A.y)*2
|
||||
},
|
||||
|
||||
nc1 = {
|
||||
x: S.x + (v1.x-S.x)*2,
|
||||
y: S.y + (v1.y-S.y)*2
|
||||
},
|
||||
nc2 = {
|
||||
x: E.x + (v2.x-E.x)*2,
|
||||
y: E.y + (v2.y-E.y)*2
|
||||
};
|
||||
return [nc1, nc2];
|
||||
},
|
||||
|
||||
draw: function(api, curve) {
|
||||
api.reset();
|
||||
|
||||
api.setColor("lightgrey");
|
||||
api.drawGrid(1,1);
|
||||
api.setColor("rgba(255,0,0,0.4)");
|
||||
api.drawCircle({x:api.w/2,y:api.h/2},api.r);
|
||||
api.setColor("transparent");
|
||||
api.setFill("rgba(100,255,100,0.4)");
|
||||
var p = {
|
||||
x: api.w/2,
|
||||
y: api.h/2,
|
||||
r: api.r,
|
||||
s: api.angle < 0 ? api.angle : 0,
|
||||
e: api.angle < 0 ? 0 : api.angle,
|
||||
};
|
||||
api.drawArc(p);
|
||||
|
||||
// guessed curve
|
||||
var B = {
|
||||
x: api.w/2 + api.r * cos(api.angle/2),
|
||||
y: api.w/2 + api.r * sin(api.angle/2)
|
||||
};
|
||||
var S = curve.points[0],
|
||||
E = curve.points[3],
|
||||
nc = this.guessCurve(S,B,E);
|
||||
var guess = new api.Bezier([S, nc[0], nc[1], E]);
|
||||
api.setColor("rgb(140,140,255)");
|
||||
api.drawLine(guess.points[0], guess.points[1]);
|
||||
api.drawLine(guess.points[1], guess.points[2]);
|
||||
api.drawLine(guess.points[2], guess.points[3]);
|
||||
api.setColor("blue");
|
||||
api.drawCurve(guess);
|
||||
api.drawCircle(guess.points[1], 3);
|
||||
api.drawCircle(guess.points[2], 3);
|
||||
|
||||
// real curve
|
||||
var offset = {x:api.w, y:0};
|
||||
api.drawSkeleton(curve);
|
||||
api.setColor("black");
|
||||
api.drawLine(curve.points[1], curve.points[2]);
|
||||
api.drawCurve(curve);
|
||||
},
|
||||
|
||||
onMouseMove: function(evt, api) {
|
||||
var x = evt.offsetX - api.w/2,
|
||||
y = evt.offsetY - api.h/2;
|
||||
if (x>api.w/2) return;
|
||||
|
||||
var angle = Math.atan2(y,x);
|
||||
if (angle < 0) {
|
||||
angle = 2*Math.PI + angle;
|
||||
}
|
||||
var pts = api.curve.points;
|
||||
// new control 1
|
||||
var r = api.r,
|
||||
f = (4 * tan(angle/4)) /3;
|
||||
pts[1] = {
|
||||
x: api.w/2 + r,
|
||||
y: api.w/2 + r * f
|
||||
};
|
||||
// new control 2
|
||||
pts[2] = {
|
||||
x: api.w/2 + api.r * (cos(angle) + f*sin(angle)),
|
||||
y: api.w/2 + api.r * (sin(angle) - f*cos(angle))
|
||||
};
|
||||
// new endpoint
|
||||
pts[3] = {
|
||||
x: api.w/2 + api.r * cos(angle),
|
||||
y: api.w/2 + api.r * sin(angle)
|
||||
};
|
||||
api.setCurve(new api.Bezier(pts));
|
||||
api.angle = angle;
|
||||
},
|
||||
|
||||
drawCircle: function(api) {
|
||||
api.setSize(325,325);
|
||||
api.reset();
|
||||
var w = api.getPanelWidth(),
|
||||
h = api.getPanelHeight(),
|
||||
pad = 60,
|
||||
r = w/2 - pad,
|
||||
k = 0.55228,
|
||||
offset = {x: -pad/2, y:0};
|
||||
var curve = new api.Bezier([
|
||||
{x:w/2 + r, y:h/2},
|
||||
{x:w/2 + r, y:h/2 + k*r},
|
||||
{x:w/2 + k*r, y:h/2 + r},
|
||||
{x:w/2, y:h/2 + r}
|
||||
]);
|
||||
api.setColor("lightgrey");
|
||||
api.drawLine({x:0,y:h/2}, {x:w+pad,y:h/2}, offset);
|
||||
api.drawLine({x:w/2,y:0}, {x:w/2,y:h}, offset);
|
||||
|
||||
var pts = curve.points;
|
||||
|
||||
api.setColor("red");
|
||||
api.drawCircle(pts[0], 3, offset);
|
||||
api.drawCircle(pts[1], 3, offset);
|
||||
api.drawCircle(pts[2], 3, offset);
|
||||
api.drawCircle(pts[3], 3, offset);
|
||||
api.drawCurve(curve, offset);
|
||||
api.setColor("rgb(255,160,160)");
|
||||
api.drawLine(pts[0],pts[1],offset);
|
||||
api.drawLine(pts[1],pts[2],offset);
|
||||
api.drawLine(pts[2],pts[3],offset);
|
||||
|
||||
api.setFill("red");
|
||||
api.text((pts[0].x - w/2) + "," + (pts[0].y - h/2), {x: pts[0].x + 7, y: pts[0].y + 3}, offset);
|
||||
api.text((pts[1].x - w/2) + "," + (pts[1].y - h/2), {x: pts[1].x + 7, y: pts[1].y + 3}, offset);
|
||||
api.text((pts[2].x - w/2) + "," + (pts[2].y - h/2), {x: pts[2].x + 7, y: pts[2].y + 7}, offset);
|
||||
api.text((pts[3].x - w/2) + "," + (pts[3].y - h/2), {x: pts[3].x, y: pts[3].y + 13}, offset);
|
||||
|
||||
pts.forEach(p => {
|
||||
p.x = -(p.x - w);
|
||||
});
|
||||
api.setColor("blue");
|
||||
api.drawCurve(curve, offset);
|
||||
|
||||
pts.forEach(p => {
|
||||
p.y = -(p.y - h);
|
||||
});
|
||||
api.setColor("green");
|
||||
api.drawCurve(curve, offset);
|
||||
|
||||
pts.forEach(p => {
|
||||
p.x = -(p.x - w);
|
||||
});
|
||||
api.setColor("purple");
|
||||
api.drawCurve(curve, offset);
|
||||
|
||||
api.setColor("black");
|
||||
api.setFill("black");
|
||||
api.drawLine({x:w/2, y:h/2}, {x:w/2 + r -2, y:h/2}, offset);
|
||||
api.drawLine({x:w/2, y:h/2}, {x:w/2, y:h/2 + r -2}, offset);
|
||||
api.text("r = " + r, {x:w/2 + r/3, y:h/2 + 10}, offset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>In the previous section we tried to approximate a circular arc with a quadratic curve,
|
||||
and it mostly made us unhappy. Cubic curves are much better suited to this task, so what
|
||||
do we need to do?</p>
|
||||
|
||||
<p>For cubic curves, we basically want the curve to pass through three points on the circle:
|
||||
the start point, the mid point at "angle/2", and the end point at "angle". We then also need
|
||||
to make sure the control points are such that the start and end tangent lines line up with the
|
||||
circle's tangent lines at the start and end point.</p>
|
||||
|
||||
<p>The first thing we can do is "guess" what the curve should look like, based on the previously
|
||||
outlined curve-through-three-points procedure. This will give use a curve with correct start, mid
|
||||
and end points, but possibly incorrect derivatives at the start and end, because the control points
|
||||
might not be in the right spot. We can then slide the control points along the lines that connect
|
||||
them to their respective end point, until they effect the corrected derivative at the start and
|
||||
end points. However, if you look back at the section on fitting curves through three points, the
|
||||
rules used were such that they optimized for a near perfect hemisphere, so using the same guess
|
||||
won't be all that useful: guessing the solution based on knowing the solution is not really guessing.</p>
|
||||
|
||||
<p>So have a graphical look at a "bad" guess versus the true fit, where we'll be using the
|
||||
bad guess and the description in the second paragraph to derive the maths for the true fit:</p>
|
||||
|
||||
<Graphic preset="arcfitting" title="Cubic Bézier arc approximation" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
|
||||
<p>We see two curves here; in blue, our "guessed" curve and its control points, and in grey/black,
|
||||
the true curve fit, with proper control points that were shifted in, along line between our guessed
|
||||
control points, such that the derivatives at the start and end points are correct.</p>
|
||||
|
||||
<p>We can already seethat cubic curves are a lot better than quadratic curves, and don't look all
|
||||
that wrong until we go well past a quarter circle; ⅜th starts to hint at problems, and half a circle
|
||||
has an obvious "gap" between the real circle and the cubic approximation. Anything past that just looks
|
||||
plain ridiculous... but quarter curves actually look pretty okay!</p>
|
||||
|
||||
<p>So, maths time again: how okay is "okay"? Let's apply some more maths to find out.</p>
|
||||
|
||||
<p>Unlike for the quadratic curve, we can't use <i>t=0.5</i> as our reference point because by its
|
||||
very nature it's one of the three points that are actually guaranteed to lie on the circular curve.
|
||||
Instead, we need a different <i>t</i> value. If we run some analysis on the curve we find that the
|
||||
actual <i>t</i> value at which the curve is furthest from what it should be is 0.211325 (rounded),
|
||||
but we don't know "why", since finding this value involves root-finding, and is nearly impossible
|
||||
to do symbolically without pages and pages of math just to express one of the possible solutions.</p>
|
||||
|
||||
<p>So instead of walking you through the derivation for that value, let's simply take that <i>t</i> value
|
||||
and see what the error is for circular arcs with an angle ranging from 0 to 2π:</p>
|
||||
|
||||
<table><tbody><tr><td>
|
||||
<p><img src="images/arc-c-2pi.gif"/></p>
|
||||
<p>plotted for 0 ≤ φ ≤ 2π:</p>
|
||||
</td><td>
|
||||
<p><img src="images/arc-c-pi.gif"/></p>
|
||||
<p>plotted for 0 ≤ φ ≤ π:</p>
|
||||
</td><td>
|
||||
<p><img src="images/arc-c-pi2.gif"/></p>
|
||||
<p>plotted for 0 ≤ φ ≤ ½π:</p>
|
||||
</td></tr></tbody></table>
|
||||
|
||||
<p>We see that cubic Bézier curves are much better when it comes to approximating circular arcs,
|
||||
with an error of less than 0.027 at the two "bulge" points for a quarter circle (which had an
|
||||
error of 0.06 for quadratic curves at the mid point), and an error near 0.001 for an eighth
|
||||
of a circle, so we're getting less than half the error for a quarter circle, or: at a slightly
|
||||
lower error, we're getting twice the arc. This makes cubic curves quite useful!</p>
|
||||
|
||||
<p>In fact, the precision of a cubic curve at a quarter circle is considered "good enough" by
|
||||
so many people that it's generally considered "just fine" to use four cubic Bézier curves to
|
||||
fake a full circle when no circle primitives are available; generally, people won't notice
|
||||
that it's not a real circle unless you also happen to overlay an actual circle, so that
|
||||
the difference becomes obvious.</p>
|
||||
|
||||
<p>So with the error analysis out of the way, how do we actually compute the coordinates
|
||||
needed to get that "true fit" cubic curve? The first observation is that we already know
|
||||
the start and end points, because they're the same as for the quadratic attempt:</p>
|
||||
|
||||
<p>\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]</p>
|
||||
|
||||
<p>But we now need to find two control points, rather than one. If we want the derivatives
|
||||
at the start and end point to match the circle, then the first control point can only lie
|
||||
somewhere on the vertical line through S, and the second control point can only lie somewhere
|
||||
on the line tangent to point E, which means:</p>
|
||||
|
||||
<p>\[
|
||||
C_1 = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>where "a" is some scaling factor, and:</p>
|
||||
|
||||
<p>\[
|
||||
C_2 = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
|
||||
\]</p>
|
||||
|
||||
<p>where "b" is also some scaling factor.</p>
|
||||
|
||||
<p>Starting with this information, we slowly maths our way to success, but I won't lie: the maths for
|
||||
this is pretty trig-heavy, and it's easy to get lost if you remember (or know!) some of the core
|
||||
trigonoetric identities, so if you just want to see the final result just skip past the next section!</p>
|
||||
|
||||
<div className="note">
|
||||
<h2>Let's do this thing.</h2>
|
||||
|
||||
<p>Unlike for the quadratic case, we need some more information in order to compute <i>a</i> and <i>b</i>,
|
||||
since they're no longer dependent variables. First, we observe that the curve is symmetrical, so whatever
|
||||
values we end up finding for C<sub>1</sub> will apply to C<sub>2</sub> as well (rotated along its tangent),
|
||||
so we'll focus on finding the location of C<sub>1</sub> only. So here's where we do something that you might
|
||||
not expect: we're going to ignore for a moment, because we're going to have a much easier time if we just
|
||||
solve this problem with geometry first, then move to calculus to solve a much simpler problem.</p>
|
||||
|
||||
<p>If we look at the triangle that is formed between our starting point, or initial guess C<sub>1</sub>
|
||||
and our real C<sub>1</sub>, there's something funny going on: if we treat the line {start,guess} as
|
||||
our opposite side, the line {guess,real} as our adjacent side, with {start,real} our hypothenuse, then
|
||||
the angle for the corner hypothenuse/adjacent is half that of the arc we're covering. Try it: if you
|
||||
place the end point at a quarter circle (pi/2, or 90 degrees), the angle in our triangle is half a
|
||||
quarter (pi/4, or 45 degrees). With that knowledge, and a knowledge of what the length of any of
|
||||
our lines segments are (as a function), we can determine where our control points are, and thus have
|
||||
everything we need to find the error distance function. Of the three lines, the one we can easiest
|
||||
determine is {start,guess}, so let's find out what the guessed control point is. Again geometrically,
|
||||
because we have the benefit of an on-curve <i>t=0.5</i> value.</p>
|
||||
|
||||
<p>The distance from our guessed point to the start point is exactly the same as the projection distance
|
||||
we looked at earlier. Using <i>t=0.5</i> as our point "B" in the "A,B,C" projection, then we know the
|
||||
length of the line segment {C,A}, since it's d<sub>1</sub> = {A,B} + d<sub>2</sub> = {B,C}:</p>
|
||||
|
||||
<p>\[
|
||||
||{A,C}|| = d_2 + d_1 = d_2 + d_2 \cdot ratio_3 \left(\frac{1}{2}\right) = d_2 + \frac{1}{3}d_2 = \frac{4}{3}d_2
|
||||
\]</p>
|
||||
|
||||
<p>So that just leaves us to find the distance from <i>t=0.5</i> to the baseline for an arbitrary
|
||||
angle φ, which is the distance from the centre of the circle to our <i>t=0.5</i> point, minus the
|
||||
distance from the centre to the line that runs from start point to end point. The first is the
|
||||
same as the point P we found for the quadratic curve:</p>
|
||||
|
||||
<p>\[
|
||||
P_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
|
||||
\]</p>
|
||||
|
||||
<p>And the distance from the origin to the line start/end is another application of angles,
|
||||
since the triangle {origin,start,C} has known angles, and two known sides. We can find
|
||||
the length of the line {origin,C}, which lets us trivially compute the coordinate for C:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
l = cos(\frac{φ}{2}) \ , \\
|
||||
\left\{\begin{array}{l}
|
||||
C_x = l \cdot cos\left(\frac{φ}{2}\right) = cos^2\left(\frac{φ}{2}\right)\ , \\
|
||||
C_y = l \cdot sin\left(\frac{φ}{2}\right) = cos(\frac{φ}{2}) \cdot sin\left(\frac{φ}{2}\right)\ , \\
|
||||
\end{array}\right.
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>With the coordinate C, and knowledge of coordinate B, we can determine coordinate A, and get a vector
|
||||
that is identical to the vector {start,guess}:</p>
|
||||
|
||||
<p>\[\left\{\begin{array}{l}
|
||||
B_x - C_x = cos\left(\frac{φ}{2}\right) - cos^2\left(\frac{φ}{2}\right) \\
|
||||
B_y - C_y = sin\left(\frac{φ}{2}\right) - cos(\frac{φ}{2}) \cdot sin\left(\frac{φ}{2}\right)
|
||||
= sin\left(\frac{φ}{2}\right) - \frac{sin(φ)}{2}
|
||||
\end{array}\right.\]</p>
|
||||
|
||||
<p>\[\left\{\begin{array}{l}
|
||||
\vec{v}_x = \{C,A\}_x = \frac{4}{3} \cdot (B_x - C_x) \\
|
||||
\vec{v}_y = \{C,A\}_y = \frac{4}{3} \cdot (B_y - C_y)
|
||||
\end{array}\right.\]</p>
|
||||
|
||||
<p>Which means we can now determine the distance {start,guessed}, which is the same as the distance
|
||||
{C,A}, and use that to determine the vertical distance from our start point to our C<sub>1</sub>:</p>
|
||||
|
||||
<p>\[\left\{\begin{array}{l}
|
||||
C_{1x} = 1 \\
|
||||
C_{1y} = \frac{d}{sin\left(\frac{φ}{2}\right)}
|
||||
= \frac{\sqrt{\vec{v}^2_x + \vec{v}^2_y}}{sin\left(\frac{φ}{2}\right)}
|
||||
= \frac{4}{3} tan \left( \frac{φ}{4} \right)
|
||||
\end{array}\right.\]</p>
|
||||
|
||||
<p>And after this tedious detour to find the coordinate for C<sub>1</sub>, we can
|
||||
find C<sub>2</sub> fairly simply, since it's lies at distance -C<sub>1y</sub> along the end point's tangent:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
E'_x = -sin(φ) \ , \ E'_y = cos(φ) \ , \ ||E'|| = \sqrt{ (-sin(φ))^2 + cos^2(φ)} = 1 \ , \\
|
||||
\left\{\begin{array}{l}
|
||||
C_2x = E_x - C_{1y} \cdot \frac{E_x'}{||E'||}
|
||||
= cos(φ) + C_{1y} \cdot sin(φ)
|
||||
= cos(φ) + \frac{4}{3} tan \left( \frac{φ}{4} \right) \cdot sin(φ) \\
|
||||
C_2y = E_y - C_{1y} \cdot \frac{E_y'}{||E'||}
|
||||
= sin(φ) - C_{1y} \cdot cos(φ)
|
||||
= sin(φ) - \frac{4}{3} tan \left( \frac{φ}{4} \right) \cdot cos(φ)
|
||||
\end{array}\right.
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>And that's it, we have all four points now for an approximation of an arbitrary
|
||||
circular arc with angle φ.</p>
|
||||
</div>
|
||||
|
||||
<p>So, to recap, given an angle φ, the new control coordinates are:</p>
|
||||
|
||||
<p>\[
|
||||
C_1 = \left [ \begin{matrix}
|
||||
1 \\
|
||||
f
|
||||
\end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
|
||||
\]</p>
|
||||
|
||||
<p>and</p>
|
||||
|
||||
<p>\[
|
||||
C_2 = \left [ \begin{matrix}
|
||||
cos(φ) + f \cdot sin(φ) \\
|
||||
sin(φ) - f \cdot cos(φ)
|
||||
\end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
|
||||
\]</p>
|
||||
|
||||
<p>And, because the "quarter curve" special case comes up so incredibly often, let's look at what
|
||||
these new control points mean for the curve coordinates of a quarter curve, by simply filling in
|
||||
φ = π/2:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
S = (1, 0) \ , \
|
||||
C_1 = \left ( 1, 4 \frac{\sqrt{2}-1}{3} \right ) \ , \
|
||||
C_2 = \left ( 4 \frac{\sqrt{2}-1}{3} , 1 \right ) \ , \
|
||||
E = (0, 1)
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>Which, in decimal values, rounded to six significant digits, is:</p>
|
||||
|
||||
<p>\[\begin{array}{l}
|
||||
S = (1, 0) \ , \
|
||||
C_1 = (1, 0.55228) \ , \
|
||||
C_2 = (0.55228 , 1) \ , \
|
||||
E = (0, 1)
|
||||
\end{array}\]</p>
|
||||
|
||||
<p>Of course, this is for a circle with radius 1, so if you have a different radius circle,
|
||||
simply multiply the coordinate by the radius you need. And then finally, forming a full curve
|
||||
is now a simple a matter of mirroring these coordinates about the origin:</p>
|
||||
|
||||
<Graphic preset="simple" title="Cubic Bézier circle approximation" draw={this.drawCircle} static={true}/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CirclesCubic;
|
93
components/sections/graduatedoffset/index.js
Normal file
93
components/sections/graduatedoffset/index.js
Normal file
@@ -0,0 +1,93 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var GraduatedOffsetting = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Graduated curve offsetting"
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api, curve) {
|
||||
api.setCurve(curve);
|
||||
api.distance = 20;
|
||||
},
|
||||
|
||||
setupQuadratic: function(api) {
|
||||
var curve = api.getDefaultQuadratic();
|
||||
this.setup(api, curve);
|
||||
},
|
||||
|
||||
setupCubic: function(api) {
|
||||
var curve = api.getDefaultCubic();
|
||||
this.setup(api, curve);
|
||||
},
|
||||
|
||||
draw: function(api, curve) {
|
||||
api.reset();
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
|
||||
|
||||
api.setColor("blue");
|
||||
var outline = curve.outline(0,0,api.distance,api.distance);
|
||||
outline.curves.forEach(c => api.drawCurve(c));
|
||||
},
|
||||
|
||||
values: {
|
||||
"38": 1, // up arrow
|
||||
"40": -1, // down arrow
|
||||
},
|
||||
|
||||
onKeyDown: function(e, api) {
|
||||
var v = this.values[e.keyCode];
|
||||
if(v) {
|
||||
e.preventDefault();
|
||||
api.distance += v;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>What if we want to do graduated offsetting, starting at some distance <i>s</i> but ending
|
||||
at some other distance <i>e</i>? well, if we can compute the length of a curve (which we can
|
||||
if we use the Legendre-Gauss quadrature approach) then we can also determine how far "along the
|
||||
line" any point on the curve is. With that knowledge, we can offset a curve so that its offset
|
||||
curve is not uniformly wide, but graduated between with two different offset widths at the
|
||||
start and end.</p>
|
||||
|
||||
<p>Like normal offsetting we cut up our curve in sub-curves, and then check at which distance
|
||||
along the original curve each sub-curve starts and ends, as well as to which point on the curve
|
||||
each of the control points map. This gives us the distance-along-the-curve for each interesting
|
||||
point in the sub-curve. If we call the total length of all sub-curves seen prior to seeing "the\
|
||||
current" sub-curve <i>S</i> (and if the current sub-curve is the first one, <i>S</i> is zero),
|
||||
and we call the full length of our original curve <i>L</i>, then we get the following graduation
|
||||
values:</p>
|
||||
|
||||
<ul>
|
||||
<li>start: map <i>S</i> from interval (<i>0,L</i>) to interval <i>(s,e)</i></li>
|
||||
<li>c1: <i>map(<strong>S+d1</strong>, 0,L, s,e)</i>, d1 = distance along curve to projection of c1</li>
|
||||
<li>c2: <i>map(<strong>S+d2</strong>, 0,L, s,e)</i>, d2 = distance along curve to projection of c2</li>
|
||||
<li>...</li>
|
||||
<li>end: <i>map(<strong>S+length(subcurve)</strong>, 0,L, s,e)</i></li>
|
||||
</ul>
|
||||
|
||||
<p>At each of the relevant points (start, end, and the projections of the control points onto
|
||||
the curve) we know the curve's normal, so offsetting is simply a matter of taking our original
|
||||
point, and moving it along the normal vector by the offset distance for each point. Doing so
|
||||
will give us the following result (these have with a starting width of 0, and an end width
|
||||
of 40 pixels, but can be controlled with your up and down cursor keys):</p>
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.onKeyDown}/>
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.onKeyDown}/>
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = GraduatedOffsetting;
|
@@ -37,31 +37,23 @@ module.exports = {
|
||||
pointcurves: require("./pointcurves"),
|
||||
|
||||
catmullconv: require("./catmullconv"),
|
||||
catmullmoulding: require("./catmullmoulding")
|
||||
};
|
||||
catmullmoulding: require("./catmullmoulding"),
|
||||
|
||||
/*
|
||||
// This requires bezier.js to have a proper poly implementation
|
||||
polybezier: require("./polybezier"),
|
||||
*/
|
||||
|
||||
/*
|
||||
polybezier: require("./polybezier"),
|
||||
shapes: require("./shapes"),
|
||||
/*
|
||||
// This section is way too much work to port, and not worth implementing given paper.js etc.
|
||||
shapes: require("./shapes"), // Boolean shape operations
|
||||
*/
|
||||
|
||||
projections: require("./projections"),
|
||||
|
||||
offsetting: require("./offsetting"),
|
||||
graduatedoffset: require("./graduatedoffset"),
|
||||
|
||||
circles: require("./circles"),
|
||||
circles_cubic: require("./circles_cubic"),
|
||||
arcapproximation: require("./arcapproximation")
|
||||
*/
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
};
|
||||
|
172
components/sections/offsetting/index.js
Normal file
172
components/sections/offsetting/index.js
Normal file
@@ -0,0 +1,172 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Offsetting = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Curve offsetting"
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api, curve) {
|
||||
api.setCurve(curve);
|
||||
api.distance = 20;
|
||||
},
|
||||
|
||||
setupQuadratic: function(api) {
|
||||
var curve = api.getDefaultQuadratic();
|
||||
this.setup(api, curve);
|
||||
},
|
||||
|
||||
setupCubic: function(api) {
|
||||
var curve = api.getDefaultCubic();
|
||||
this.setup(api, curve);
|
||||
},
|
||||
|
||||
draw: function(api, curve) {
|
||||
api.reset();
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
|
||||
api.setColor("red");
|
||||
var offset = curve.offset(api.distance);
|
||||
offset.forEach(c => api.drawCurve(c));
|
||||
|
||||
api.setColor("blue");
|
||||
offset = curve.offset(-api.distance);
|
||||
offset.forEach(c => api.drawCurve(c));
|
||||
},
|
||||
|
||||
values: {
|
||||
"38": 1, // up arrow
|
||||
"40": -1, // down arrow
|
||||
},
|
||||
|
||||
onKeyDown: function(e, api) {
|
||||
var v = this.values[e.keyCode];
|
||||
if(v) {
|
||||
e.preventDefault();
|
||||
api.distance += v;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Perhaps you are like me, and you've been writing various small programs that use Bézier curves in some way or another,
|
||||
and at some point you make the step to implementing path extrusion. But you don't want to do it pixel based, you want to
|
||||
stay in the vector world. You find that extruding lines is relatively easy, and tracing outlines is coming along nicely
|
||||
(although junction caps and fillets are a bit of a hassle), and then decide to do things properly and add Bézier curves
|
||||
to the mix. Now you have a problem.</p>
|
||||
|
||||
<p>Unlike lines, you can't simply extrude a Bézier curve by taking a copy and moving it around, because of the curvatures;
|
||||
rather than a uniform thickness you get an extrusion that looks too thin in places, if you're lucky, but more likely will
|
||||
self-intersect. The trick, then, is to scale the curve, rather than simply copying it. But how do you scale a Bézier curve?</p>
|
||||
|
||||
<p>Bottom line: <strong>you can't</strong>. So you cheat. We're not going to do true curve scaling, or rather curve
|
||||
offsetting, because that's impossible. Instead we're going to try to generate 'looks good enough' offset curves.</p>
|
||||
|
||||
<div className="note">
|
||||
<h2>"What do you mean, you can't. Prove it."</h2>
|
||||
|
||||
<p>First off, when I say "you can't" what I really mean is "you can't offset a Bézier curve with another
|
||||
Bézier curve". not even by using a really high order curve. You can find the function that describes the
|
||||
offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which
|
||||
<strong>has</strong> to be a polynomial. Let's look at why this is:</p>
|
||||
|
||||
<p>From a mathematical point of view, an offset curve <i>O(t)</i> is a curve such that, given our original curve
|
||||
<i>B(t)</i>, any point on <i>O(t)</i> is a fixed distance <i>d</i> away from coordinate <i>B(t)</i>.
|
||||
So let's math that:</p>
|
||||
|
||||
<p>\[
|
||||
O(t) = B(t) + d
|
||||
\]</p>
|
||||
|
||||
<p>However, we're working in 2D, and <i>d</i> is a single value, so we want to turn it into a vector. If we
|
||||
want a point distance <i>d</i> "away" from the curve <i>B(t)</i> then what we really mean is that we want
|
||||
a point at <i>d</i> times the "normal vector" from point <i>B(t)</i>, where the "normal" is a vector
|
||||
that runs perpendicular ("at a right angle") to the tangent at <i>B(t)</i>. Easy enough:</p>
|
||||
|
||||
<p>\[
|
||||
O(t) = B(t) + d \cdot N(t)
|
||||
\]</p>
|
||||
|
||||
<p>Now this still isn't very useful unless we know what the formula for <i>N(t)</i> is, so let's find out.
|
||||
<i>N(t)</i> runs perpendicular to the original curve tangent, and we know that the tangent is simply
|
||||
<i>B'(t)</i>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure
|
||||
that <i>N(t)</i> has the same magnitude for every <i>t</i>, or the offset curve won't be at a uniform
|
||||
distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure
|
||||
<i>N(t)</i> always has length 1, which we can achieve by dividing <i>B'(t)</i> by its magnitude:</p>
|
||||
|
||||
<p>\[
|
||||
N(t) \perp \left ( \frac{B'(t)}{\left || B'(t) \right || } \right )
|
||||
\]</p>
|
||||
|
||||
<p>Determining the length requires computing an arc length, and this is where things get Tricky with
|
||||
a capital T. First off, to compute arc length from some start <i>a</i> to end <i>b</i>, we must use
|
||||
the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p>
|
||||
|
||||
<p>\[
|
||||
\left || f(x,y) \right || = \int^b_a \sqrt{ f_x'^2 + f_y'^2}
|
||||
\]</p>
|
||||
|
||||
<p>So if we want the length of the tangent, we plug in <i>B'(t)</i>, with <i>t = 0</i> as start and
|
||||
<i>t = 1</i> as end:</p>
|
||||
|
||||
<p>\[
|
||||
\left || B'(t) \right || = \int^1_0 \sqrt{ B_x''(t)^2 + B_y''(t)^2}
|
||||
\]</p>
|
||||
|
||||
<p>And that's where things go wrong. It doesn't even really matter what the second derivative for <i>B(t)</i>
|
||||
is, that square root is screwing everything up, because it turns our nice polynomials into things that are no
|
||||
longer polynomials.</p>
|
||||
|
||||
<p>There is a small class of polynomials where the square root is also a polynomial, but
|
||||
they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is
|
||||
also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't;
|
||||
remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor
|
||||
in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions
|
||||
stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point.
|
||||
We can already create offset curves for points, we call them circles, and they have much simpler functions
|
||||
than Bézier curves.</p>
|
||||
|
||||
<p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which
|
||||
means <i>N(t)</i> won't be a polynomial, which means that <i>d</i> times <i>N(t)</i> won't be a polynomial,
|
||||
which means that, ultimately, <i>O(t)</i> won't be a polynomial, which means that even if we can determine the
|
||||
function for <i>O(t)</i> just fine (and that's far from trivial!), it simply cannot be represented as a
|
||||
Bézier curve.</p>
|
||||
|
||||
<p>And that's one reason why Bézier curves are tricky: there are actually a <i>lot</i> of curves that
|
||||
cannot be represent as a Bézier curve at all. They can't even model their own offset curves. They're weird
|
||||
that way. So how do all those other programs do it? Well, much like we're about to do, they cheat. We're
|
||||
going to approximate an offset curve in a way that will look relatively close to what the real offset
|
||||
curve would look like, if we could compute it.</p>
|
||||
</div>
|
||||
|
||||
<p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make
|
||||
that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where safe means that all the
|
||||
control points are always on a single side of the baseline, and the midpoint of the curve at <i>t=0.5</i> is
|
||||
roughly in the centre of the polygon defined by the curve coordinates) and then point-scale those sub-curves
|
||||
with respect to the curve's scaling origin (which is the intersection of the point normals at the start
|
||||
and end points).</p>
|
||||
|
||||
<p>The following graphics show off curve offsetting, and you can use your up and down cursor keys to control
|
||||
the distance at which the curve gets offset:</p>
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.onKeyDown} />
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.onKeyDown} />
|
||||
|
||||
<p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the
|
||||
curve around. This is caused by the fact that we're still performing a naive form of offsetting,
|
||||
moving the control points the same distance as the start and end points. If the curve is large
|
||||
enough, this may still lead to incorrect offsets.</p>
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Offsetting;
|
172
components/sections/polybezier/index.js
Normal file
172
components/sections/polybezier/index.js
Normal file
@@ -0,0 +1,172 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var PolyBezier = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Forming poly-Bézier curves"
|
||||
};
|
||||
},
|
||||
|
||||
setupQuadratic: function(api) {
|
||||
var w = api.getPanelWidth(),
|
||||
h = api.getPanelHeight(),
|
||||
cx = w/2, cy = h/2, pad = 40,
|
||||
pts = [
|
||||
// first curve:
|
||||
{x:cx,y:pad}, {x:w-pad,y:pad}, {x:w-pad,y:cy},
|
||||
// subsequent curve
|
||||
{x:w-pad,y:h-pad}, {x:cx,y:h-pad},
|
||||
// subsequent curve
|
||||
{x:pad,y:h-pad}, {x:pad,y:cy},
|
||||
// final curve control point
|
||||
{x:pad,y:pad},
|
||||
];
|
||||
api.lpts = pts;
|
||||
},
|
||||
|
||||
setupCubic: function(api) {
|
||||
},
|
||||
|
||||
draw: function(api, curves) {
|
||||
api.reset();
|
||||
var pts = api.lpts;
|
||||
|
||||
var c1 = new api.Bezier(pts[0],pts[1],pts[2]);
|
||||
api.drawSkeleton(c1);
|
||||
api.drawCurve(c1);
|
||||
|
||||
var c2 = new api.Bezier(pts[2],pts[3],pts[4]);
|
||||
api.drawSkeleton(c2);
|
||||
api.drawCurve(c2);
|
||||
|
||||
var c3 = new api.Bezier(pts[4],pts[5],pts[6]);
|
||||
api.drawSkeleton(c3);
|
||||
api.drawCurve(c3);
|
||||
|
||||
var c4 = new api.Bezier(pts[6],pts[7],pts[0]);
|
||||
api.drawSkeleton(c4);
|
||||
api.drawCurve(c4);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Much like lines can be chained together to form polygons, Bézier curves can be chained together
|
||||
to form poly-Béziers, and the only trick required is to make sure that: A) the end point of each
|
||||
section is the starting point of the following section, and B) the derivatives across that
|
||||
dual point line up. Unless, of course, you want discontinuities; then you don't even need (B).</p>
|
||||
|
||||
<p>We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind
|
||||
that enforces "the outgoing derivative is the same as the incoming derivative" across sections:</p>
|
||||
|
||||
<p>\[
|
||||
B'(1)_n = B'(0)_{n+1}
|
||||
\]</p>
|
||||
|
||||
<p>We can actually guarantee this really easily, because we know that the vector from a curve's
|
||||
last control point to its last on-curve point is equal to the derivative vector. If we want to
|
||||
ensure that the first control point of the next curve matches that, all we have to do is mirror
|
||||
that last control point through the last on-curve point. And mirroring any point A through any
|
||||
point B is really simple:</p>
|
||||
|
||||
<p>\[
|
||||
Mirrored = \left [
|
||||
\begin{matrix} B_x + (B_x - A_x) \\ B_y + (B_y - A_y) \end{matrix}
|
||||
\right ] = \left [
|
||||
\begin{matrix} 2B_x - A_x \\ 2B_y - A_y \end{matrix}
|
||||
\right ]
|
||||
\]</p>
|
||||
|
||||
<p>So let's implement that and see what it gets us. The following two graphics show a quadratic
|
||||
and a cubic poly-Bézier curve; both consist of multiple sub-curves, but because of our constraint,
|
||||
not all points on the curves can be moved around freely. Some points, when moved, will move other
|
||||
points by virtue of changing the curve across sections.</p>
|
||||
|
||||
<Graphic preset="poly" title="Forming a quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
|
||||
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="Forming a cubic poly-Bézier">
|
||||
void setupCurve() {
|
||||
setupDefaultCubicPoly();
|
||||
}
|
||||
|
||||
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
|
||||
p.movePointConstrained(pt, mx, my);
|
||||
}</textarea>
|
||||
|
||||
<p>As you can see, quadratic curves are particularly ill-suited for poly-Bézier curves, as all
|
||||
the control points are effectively linked. Move one of them, and you move all of them. This means
|
||||
that we cannot use quadratic poly-Béziers for anything other than really, really simple shapes.
|
||||
And even then, they're probably the wrong choice. Cubic curves are pretty decent, but the fact
|
||||
that the derivatives are linked means we can't manipulate curves as well as we might if we
|
||||
relaxed the constraints a little.</p>
|
||||
|
||||
<p>So: let's relax them!</p>
|
||||
|
||||
<p>We can change the constraint so that we still preserve the angle of the derivatives across
|
||||
sections (so transitions from one section to the next will still look natural), but give up
|
||||
the requirement that they should also have the same vector length. Doing so will give us
|
||||
a much more a useful kind of poly-Bézier curve:</p>
|
||||
|
||||
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="A half-constrained quadratic poly-Bézier">
|
||||
void setupCurve() {
|
||||
setupDefaultQuadraticPoly();
|
||||
}
|
||||
|
||||
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
|
||||
p.movePointHalfConstrained(pt, mx, my);
|
||||
}</textarea>
|
||||
|
||||
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="A half-constrained cubic poly-Bézier">
|
||||
void setupCurve() {
|
||||
setupDefaultCubicPoly();
|
||||
}
|
||||
|
||||
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
|
||||
p.movePointHalfConstrained(pt, mx, my);
|
||||
}</textarea>
|
||||
|
||||
<p>Quadratic curves are still silly, but cubic curves are now much more controllable.</p>
|
||||
|
||||
<p>If we want even more control, we could just abandon the derivative constraints entirely,
|
||||
and simply assure that the end point of one section is the same as the start point of the next section,
|
||||
and then keep it at that. This gives us the greatest degree of freedom when it comes to modelling
|
||||
shapes, but also means that our poly-Bézier constructs are no longer continuous curves. Sometimes
|
||||
this is exactly what you want (because it lets you add corners to a shape, while still only using
|
||||
Bézier curves).</p>
|
||||
|
||||
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="An unconstrained quadratic poly-Bézier">
|
||||
void setupCurve() {
|
||||
setupDefaultQuadraticPoly();
|
||||
}
|
||||
|
||||
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
|
||||
p.movePoint(pvt, mx, my);
|
||||
}</textarea>
|
||||
|
||||
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="An unconstrained cubic poly-Bézier">
|
||||
void setupCurve() {
|
||||
setupDefaultCubicPoly();
|
||||
}
|
||||
|
||||
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
|
||||
p.movePoint(pvt, mx, my);
|
||||
}</textarea>
|
||||
|
||||
<p>When doing any kind of modelling, you generally don't want a poly-Bézier that will only let you
|
||||
pick one of the three forms for all your points; most graphics applications that deal with Bézier
|
||||
curves will actually let you pick, per on-curve point, how to deal with the control points around it:
|
||||
fully constrained, loosely constrained, or completely unconstrained. The best shape modelling comes
|
||||
from having a curve that will let you pick what you need, when you need it, without having to start
|
||||
a new poly-Bézier curve.</p>
|
||||
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PolyBezier;
|
102
components/sections/projections/index.js
Normal file
102
components/sections/projections/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Projections = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Projecting a point onto a Bézier curve"
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api) {
|
||||
api.setSize(320,320);
|
||||
var curve = new api.Bezier([
|
||||
{x:248,y:188},
|
||||
{x:218,y:294},
|
||||
{x:45,y:290},
|
||||
{x:12,y:236},
|
||||
{x:14,y:82},
|
||||
{x:186,y:177},
|
||||
{x:221,y:90},
|
||||
{x:18,y:156},
|
||||
{x:34,y:57},
|
||||
{x:198,y:18}
|
||||
]);
|
||||
api.setCurve(curve);
|
||||
api._lut = curve.getLUT();
|
||||
},
|
||||
|
||||
findClosest: function(LUT, p, dist) {
|
||||
var i,
|
||||
end = LUT.length,
|
||||
d,
|
||||
dd = dist(LUT[0],p),
|
||||
f = 0;
|
||||
for(i=1; i<end; i++) {
|
||||
d = dist(LUT[i],p);
|
||||
if(d<dd) {f = i;dd = d;}
|
||||
}
|
||||
return f/(end-1);
|
||||
},
|
||||
|
||||
draw: function(api, curve) {
|
||||
api.reset();
|
||||
api.drawSkeleton(curve);
|
||||
api.drawCurve(curve);
|
||||
if (api.mousePt) {
|
||||
api.setColor("red");
|
||||
api.drawCircle(api.mousePt, 3);
|
||||
// naive t value
|
||||
var t = this.findClosest(api._lut, api.mousePt, api.utils.dist);
|
||||
// no real point in refining for illustration purposes
|
||||
var p = curve.get(t);
|
||||
api.drawLine(p, api.mousePt);
|
||||
api.drawCircle(p, 3);
|
||||
}
|
||||
},
|
||||
|
||||
onMouseMove: function(evt, api) {
|
||||
api.mousePt = {x: evt.offsetX, y: evt.offsetY };
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>Say we have a Bézier curve and some point, not on the curve, of which we want to know
|
||||
which <i>t</i> value on the curve gives us an on-curve point closest to our off-curve point.
|
||||
Or: say we want to find the projection of a random point onto a curve. How do we do that?</p>
|
||||
|
||||
<p>If the Bézier curve is of low enough order, we might be able
|
||||
to <a href="http://jazzros.blogspot.ca/2011/03/projecting-point-on-bezier-curve.html">work out
|
||||
the maths for how to do this</a>, and get a perfect <i>t</i> value back, but in general this is
|
||||
an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll
|
||||
be finding our ideal <i>t</i> value using a <a href="https://en.wikipedia.org/wiki/Binary_search_algorithm">binary
|
||||
search</a>. First, we do a coarse distance-check based on <i>t</i> values associated with the
|
||||
curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run
|
||||
this algorithm:</p>
|
||||
|
||||
<ol>
|
||||
<li>with the <i>t</i> value we found, start with some small interval around <i>t</i> (1/length_of_LUT on either side is a reasonable start),</li>
|
||||
<li>if the distance to <i>t ± interval/2</i> is larger than the distance to <i>t</i>, try again with the interval reduced to half its original length.</li>
|
||||
<li>if the distance to <i>t ± interval/2</i> is smaller than the distance to <i>t</i>, replace <i>t</i> with the smaller-distance value.</li>
|
||||
<li>after reducing the interval, or changing <i>t</i>, go back to step 1.</li>
|
||||
</ol>
|
||||
|
||||
<p>We keep repeating this process until the interval is small enough to claim the difference
|
||||
in precision found is irrelevant for the purpose we're trying to find <i>t</i> for. In this
|
||||
case, I'm arbitrarily fixing it at 0.0001.</p>
|
||||
|
||||
<p>The following graphic demonstrates the result of this procedure.Simply move the cursor
|
||||
around, and if it does not lie on top of the curve, you will see a line that projects the
|
||||
cursor onto the curve based on an iteratively found "ideal" <i>t</i> value.</p>
|
||||
|
||||
<Graphic preset="simple" title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Projections;
|
@@ -158,27 +158,3 @@ var Reordering = React.createClass({
|
||||
});
|
||||
|
||||
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>
|
||||
*/
|
0
components/sections/shapes/index.js
Normal file
0
components/sections/shapes/index.js
Normal file
Reference in New Issue
Block a user