1
0
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:
Pomax
2016-01-09 18:39:09 -08:00
parent 2ee641554c
commit 9ede7b4143
47 changed files with 6520 additions and 794 deletions

View 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;

View 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;

View 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;

View 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;

View File

@@ -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
*/
};

View 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;

View 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;

View 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;

View File

@@ -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>
*/

View File