mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-09-01 04:22:28 +02:00
en sections 31-35
This commit is contained in:
@@ -133,7 +133,7 @@ var ABC = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -65,7 +65,7 @@ var Aligning = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -102,7 +102,7 @@ var Arclength = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -71,7 +71,7 @@ var ArclengthApprox = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -35,7 +35,7 @@ var BoundingBox = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -170,7 +170,7 @@ var Canonical = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -12,7 +12,7 @@ var CatmullRomConversion = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -144,7 +144,7 @@ var CatmullRomMoulding = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -50,7 +50,7 @@ var Components = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -170,7 +170,7 @@ var Control = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -14,6 +14,8 @@ This algorithm will start with a single pair, "balloon" until it runs in paralle
|
||||
|
||||
The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. The algorithm resets once it's found a solution, so you can try this with lots of different curves (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)
|
||||
|
||||
<Graphic preset="clipping" title="Curve/curve intersections" setup={this.setup} draw={this.draw} children={[<button onClick={this.stepUp}>advance one step</button>]}/>
|
||||
<Graphic preset="clipping" title="Curve/curve intersections" setup={this.setup} draw={this.draw}>
|
||||
<button onClick={this.stepUp}>advance one step</button>
|
||||
</Graphic>
|
||||
|
||||
Self-intersection is dealt with in the same way, except we turn a curve into two or more curves first based on the inflection points. We then form all possible curve pairs with the resultant segments, and run exactly the same algorithm. All non-overlapping curve pairs will be removed after the first iteration, and the remaining steps home in on the curve's self-intersection points.
|
||||
|
@@ -128,7 +128,7 @@ var CurveIntersections = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -12,7 +12,7 @@ var Derivatives = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -64,7 +64,7 @@ var Extremities = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
16
components/sections/graduatedoffset/content.en-GB.md
Normal file
16
components/sections/graduatedoffset/content.en-GB.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Graduated curve offsetting
|
||||
|
||||
What if we want to do graduated offsetting, starting at some distance `s` but ending at some other distance `e`? 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.
|
||||
|
||||
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 `S` (and if the current sub-curve is the first one, `S` is zero), and we call the full length of our original curve `L`, then we get the following graduation values:
|
||||
|
||||
- start: map `S` from interval (`0,L`) to interval `(s,e)`
|
||||
- c1: `map(<strong>S+d1</strong>, 0,L, s,e)`, d1 = distance along curve to projection of c1
|
||||
- c2: `map(<strong>S+d2</strong>, 0,L, s,e)`, d2 = distance along curve to projection of c2
|
||||
- ...
|
||||
- end: `map(<strong>S+length(subcurve)</strong>, 0,L, s,e)`
|
||||
|
||||
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 arrow keys):
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
@@ -1,6 +1,9 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "graduatedoffset";
|
||||
|
||||
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
|
||||
|
||||
var GraduatedOffsetting = React.createClass({
|
||||
@@ -16,7 +19,7 @@ var GraduatedOffsetting = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Graduated curve offsetting"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -47,43 +50,7 @@ var GraduatedOffsetting = React.createClass({
|
||||
},
|
||||
|
||||
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 arrow keys):</p>
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
</section>
|
||||
);
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -28,7 +28,7 @@ var ABC = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -86,7 +86,7 @@ var Intersections = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -210,7 +210,7 @@ var Moulding = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
65
components/sections/offsetting/content.en-GB.md
Normal file
65
components/sections/offsetting/content.en-GB.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Curve offsetting
|
||||
|
||||
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.
|
||||
|
||||
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?
|
||||
|
||||
Bottom line: **you can't**. 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.
|
||||
|
||||
<div className="note">
|
||||
|
||||
### "What do you mean, you can't. Prove it."
|
||||
|
||||
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 **has** to be a polynomial. Let's look at why this is:
|
||||
|
||||
From a mathematical point of view, an offset curve `O(t)` is a curve such that, given our original curve `B(t)`, any point on `O(t)` is a fixed distance `d` away from coordinate `B(t)`. So let's math that:
|
||||
|
||||
\[
|
||||
O(t) = B(t) + d
|
||||
\]
|
||||
|
||||
However, we're working in 2D, and `d` is a single value, so we want to turn it into a vector. If we want a point distance `d` "away" from the curve `B(t)` then what we really mean is that we want a point at `d` times the "normal vector" from point `B(t)`, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at `B(t)`. Easy enough:
|
||||
|
||||
\[
|
||||
O(t) = B(t) + d \cdot N(t)
|
||||
\]
|
||||
|
||||
Now this still isn't very useful unless we know what the formula for `N(t)` is, so let's find out. `N(t)` runs perpendicular to the original curve tangent, and we know that the tangent is simply `B'(t)`, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that `N(t)` has the same magnitude for every `t`, 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 `N(t)` always has length 1, which we can achieve by dividing `B'(t)` by its magnitude:
|
||||
|
||||
\[
|
||||
N(t) \bot \left ( \frac{B'(t)}{\left || B'(t) \right || } \right )
|
||||
\]
|
||||
|
||||
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 `a` to end `b`, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:
|
||||
|
||||
\[
|
||||
\left || f(x,y) \right || = \int^b_a \sqrt{ f_x'^2 + f_y'^2}
|
||||
\]
|
||||
|
||||
So if we want the length of the tangent, we plug in `B'(t)`, with `t = 0` as start and
|
||||
`t = 1` as end:
|
||||
|
||||
\[
|
||||
\left || B'(t) \right || = \int^1_0 \sqrt{ B_x''(t)^2 + B_y''(t)^2}
|
||||
\]
|
||||
|
||||
And that's where things go wrong. It doesn't even really matter what the second derivative for `B(t)` is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.
|
||||
|
||||
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 **base** 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.
|
||||
|
||||
So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means `N(t)` won't be a polynomial, which means that `d` times `N(t)` won't be a polynomial, which means that, ultimately, `O(t)` won't be a polynomial, which means that even if we can determine the function for `O(t)` just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.
|
||||
|
||||
And that's one reason why Bézier curves are tricky: there are actually a `lot` 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.
|
||||
|
||||
</div>
|
||||
|
||||
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 `t=0.5` 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).
|
||||
|
||||
A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for `t=0.5` is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.
|
||||
|
||||
The following graphics show off curve offsetting, and you can use your up and down arrow keys to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
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.
|
@@ -1,6 +1,9 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "offsetting";
|
||||
|
||||
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
|
||||
|
||||
var Offsetting = React.createClass({
|
||||
@@ -16,7 +19,7 @@ var Offsetting = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Curve offsetting"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -68,129 +71,7 @@ var Offsetting = React.createClass({
|
||||
},
|
||||
|
||||
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) \bot \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>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier
|
||||
section on curve extremities, and use these as initial splitting points. After this initial split, we can
|
||||
check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the
|
||||
on-curve point for <i>t=0.5</i> is too far off from the center, we simply split the segment down the middle.
|
||||
Generally this is more than enough to end up with safe segments.</p>
|
||||
|
||||
<p>The following graphics show off curve offsetting, and you can use your up and down arrow keys to control
|
||||
the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is
|
||||
then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic
|
||||
curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced
|
||||
in order to get segments that can safely be scaled.</p>
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.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>
|
||||
);
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -175,7 +175,7 @@ var PointCurves = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -44,7 +44,7 @@ var PointVectors = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
54
components/sections/polybezier/content.en-GB.md
Normal file
54
components/sections/polybezier/content.en-GB.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Forming poly-Bézier curves
|
||||
|
||||
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:
|
||||
|
||||
1. the end point of each section is the starting point of the following section, and
|
||||
2. the derivatives across that dual point line up.
|
||||
|
||||
Unless, of course, you want discontinuities; then you don't even need 2.
|
||||
|
||||
We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind that just follows point 1. where the end point of a segment is the same point as the start point of the next segment. This leads to poly-Béziers that are pretty hard to work with, but they're the easiest to implement:
|
||||
|
||||
<Graphic preset="poly" title="Unlinked quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic preset="poly" title="Unlinked cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}/>
|
||||
|
||||
Dragging the control points around only affects the curve segments that the control point belongs to, and moving an on-curve point leaves the control points where they are, which is not the most useful for practical modelling purposes. So, let's add in the logic we need to make things a little better. We'll start by linking up control points by ensuring that the "incoming" derivative at an on-curve point is the same as it's "outgoing" derivative:
|
||||
|
||||
\[
|
||||
B'(1)_n = B'(0)_{n+1}
|
||||
\]
|
||||
|
||||
We can effect this quite 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:
|
||||
|
||||
\[
|
||||
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 ]
|
||||
\]
|
||||
|
||||
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 again, but this time moving the control points around moves others, too. However, you might see something unexpected going on for quadratic curves...
|
||||
|
||||
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw} onMouseMove={this.linkDerivatives}/>
|
||||
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw} onMouseMove={this.linkDerivatives}/>
|
||||
|
||||
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. Not only that, but if we move the on-curve points, it's possible to get a situation where a control point's positions is different depending on whether it's the reflection of its left or right neighbouring control point: we can't even form a proper rule-conforming curve! 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.
|
||||
|
||||
So: let's relax the requirement a little.
|
||||
|
||||
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 useful kind of poly-Bézier curve:
|
||||
|
||||
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw} onMouseMove={this.linkDirection}/>
|
||||
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw} onMouseMove={this.linkDirection}/>
|
||||
|
||||
Cubic curves are now better behaved when it comes to dragging control points around, but the quadratic poly-Bézier still has the problem that moving one control points will move the control points and may ending up defining "the next" control point in a way that doesn't work. Quadratic curves really aren't very useful to work with...
|
||||
|
||||
Finally, we also want to make sure that moving the on-curve coordinates preserves the relative positions of the associated control points. With that, we get to the kind of curve control that you might be familiar with from applications like Photoshop, Inkscape, Blender, etc.
|
||||
|
||||
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw} onMouseDown={this.bufferPoints} onMouseMove={this.modelCurve}/>
|
||||
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw} onMouseDown={this.bufferPoints} onMouseMove={this.modelCurve}/>
|
||||
|
||||
Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...
|
||||
|
||||
A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an excercise for the reader.
|
@@ -1,13 +1,15 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "polybezier";
|
||||
|
||||
var atan2 = Math.atan2, sqrt = Math.sqrt, sin = Math.sin, cos = Math.cos;
|
||||
|
||||
var PolyBezier = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Forming poly-Bézier curves"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -250,105 +252,7 @@ var PolyBezier = React.createClass({
|
||||
},
|
||||
|
||||
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:</p>
|
||||
|
||||
<ol>
|
||||
<li>the end point of each section is the starting point of the following section, and</li>
|
||||
<li>the derivatives across that dual point line up.</li>
|
||||
</ol>
|
||||
|
||||
<p>Unless, of course, you want discontinuities; then you don't even need 2.</p>
|
||||
|
||||
<p>We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind
|
||||
that just follows point 1. where the end point of a segment is the same point as the start point
|
||||
of the next segment. This leads to poly-Béziers that are pretty hard to work with, but they're
|
||||
the easiest to implement:</p>
|
||||
|
||||
<Graphic preset="poly" title="Unlinked quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic preset="poly" title="Unlinked cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}/>
|
||||
|
||||
<p>Dragging the control points around only affects the curve segments that the control point belongs
|
||||
to, and moving an on-curve point leaves the control points where they are, which is not the most useful
|
||||
for practical modelling purposes. So, let's add in the logic we need to make things a little better.
|
||||
We'll start by linking up control points by ensuring that the "incoming" derivative at an on-curve
|
||||
point is the same as it's "outgoing" derivative:</p>
|
||||
|
||||
<p>\[
|
||||
B'(1)_n = B'(0)_{n+1}
|
||||
\]</p>
|
||||
|
||||
<p>We can effect this quite 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 again, but this time moving the control points around moves others,
|
||||
too. However, you might see something unexpected going on for quadratic curves...</p>
|
||||
|
||||
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}
|
||||
onMouseMove={this.linkDerivatives}/>
|
||||
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}
|
||||
onMouseMove={this.linkDerivatives}/>
|
||||
|
||||
<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. Not only that,
|
||||
but if we move the on-curve points, it's possible to get a situation where a control point's positions
|
||||
is different depending on whether it's the reflection of its left or right neighbouring control
|
||||
point: we can't even form a proper rule-conforming curve! 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 the requirement a little.</p>
|
||||
|
||||
<p>We can change the constraint so that we still preserve the <em>angle</em> 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 <em>vector length</em>. Doing so will give us a much
|
||||
more useful kind of poly-Bézier curve:</p>
|
||||
|
||||
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw} onMouseMove={this.linkDirection}/>
|
||||
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw} onMouseMove={this.linkDirection}/>
|
||||
|
||||
<p>Cubic curves are now better behaved when it comes to dragging control points around, but the
|
||||
quadratic poly-Bézier still has the problem that moving one control points will move
|
||||
the control points and may ending up defining "the next" control point in a way that
|
||||
doesn't work. Quadratic curves really aren't very useful to work with...</p>
|
||||
|
||||
<p>Finally, we also want to make sure that moving the on-curve coordinates preserves the relative
|
||||
positions of the associated control points. With that, we get to the kind of curve control that you
|
||||
might be familiar with from applications like Photoshop, Inkscape, Blender, etc.</p>
|
||||
|
||||
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}
|
||||
onMouseDown={this.bufferPoints} onMouseMove={this.modelCurve}/>
|
||||
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}
|
||||
onMouseDown={this.bufferPoints} onMouseMove={this.modelCurve}/>
|
||||
|
||||
<p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a
|
||||
new, very serious problem: we can move an on-curve point in such a way that we can't compute what
|
||||
needs to "happen next". Move the top point down, below the left and right points, for instance. There
|
||||
is no way to preserve correct control points without a kink at the bottom point. Quadratic curves:
|
||||
just not that good...</p>
|
||||
|
||||
<p>A final improvement is to offer fine-level control over which points behave which, so that you can
|
||||
have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves
|
||||
for the rest of the path. Implementing that, is left as an excercise for the reader.</p>
|
||||
</section>
|
||||
);
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
16
components/sections/projections/content.en-GB.md
Normal file
16
components/sections/projections/content.en-GB.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Projecting a point onto a Bézier curve
|
||||
|
||||
Say we have a Bézier curve and some point, not on the curve, of which we want to know which `t` 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?
|
||||
|
||||
If the Bézier curve is of low enough order, we might be able to [work out the maths for how to do this](http://jazzros.blogspot.ca/2011/03/projecting-point-on-bezier-curve.html), and get a perfect `t` 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 `t` value using a [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm). First, we do a coarse distance-check based on `t` values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run this algorithm:
|
||||
|
||||
1. with the `t` value we found, start with some small interval around `t` (1/length_of_LUT on either side is a reasonable start),
|
||||
2. if the distance to `t ± interval/2` is larger than the distance to `t`, try again with the interval reduced to half its original length.
|
||||
3. if the distance to `t ± interval/2` is smaller than the distance to `t`, replace `t` with the smaller-distance value.
|
||||
4. after reducing the interval, or changing `t`, go back to step 1.
|
||||
|
||||
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 `t` for. In this case, I'm arbitrarily fixing it at 0.0001.
|
||||
|
||||
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" `t` value.
|
||||
|
||||
<Graphic preset="simple" title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
@@ -1,11 +1,13 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "projections";
|
||||
|
||||
var Projections = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Projecting a point onto a Bézier curve"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -64,41 +66,7 @@ var Projections = React.createClass({
|
||||
},
|
||||
|
||||
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>
|
||||
);
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
52
components/sections/shapes/content.en-GB.md
Normal file
52
components/sections/shapes/content.en-GB.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Boolean shape operations
|
||||
|
||||
We can apply the topics covered so far in this primer to effect boolean shape operations: getting the union, intersection, or exclusion, between two or more shapes that involve Bézier curves. For simplicity (well.. sort of, more homogeneity), we'll be looking at Poly-Bézier shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a simplification (although it does mean we need to write a definition for the class of shapes that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using those in the following examples)
|
||||
|
||||
The procedure for performing boolean operations consists, broadly, of four steps:
|
||||
|
||||
1. Find the intersection points between both shapes,
|
||||
2. cut up the shapes into multiple sections between these intersections,
|
||||
3. discard any section that isn't part of the desired operation's resultant shape, and
|
||||
4. link up the remaining sections to form the new shape.
|
||||
|
||||
Finding all intersections between two poly-Bézier curves, or any poly-line-section shape, is similar to the iterative algorithm discussed in the section on curve/curve intersection. For each segment in the poly-Bézier curve we check whether its bounding box overlaps with any of the segment bounding boxes in the other poly-Bézier curve. If so, we run normal intersection detection.
|
||||
|
||||
After we found all intersection points, we split up our poly-Bézier curves, making sure to record which of the newly formed poly-Bézier curves might potentially link up at the points we split the originals up at. This will let us quickly glue poly-Bézier curves back together after the next step.
|
||||
|
||||
Once we have all the new poly-Bézier curves, we run the first step of the desired boolean operation.
|
||||
|
||||
- Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if we want the union of two overlapping circles, the resulting shape is the outline.
|
||||
- Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the two shapes. E.g. if we want the intersection of two overlapping circles, the resulting shape is the tapered ellipse where they overlap.
|
||||
- Exclusion: none of the sections are discarded, but we will need to link the shapes back up in a special way. Flip any section that would qualify for removal under UNION rules.
|
||||
|
||||
<table className="sketch"><tbody><tr>
|
||||
<td className="labeled-image">
|
||||
<img src="images/op_base.gif" height="169px"/>
|
||||
Two overlapping shapes.
|
||||
</td>
|
||||
<td className="labeled-image">
|
||||
<img src="images/op_union.gif" height="169px"/>
|
||||
The unified region.
|
||||
</td>
|
||||
<td className="labeled-image">
|
||||
<img src="images/op_intersection.gif" height="169px"/>
|
||||
Their intersection.
|
||||
</td>
|
||||
<td className="labeled-image">
|
||||
<img src="images/op_exclusion.gif" height="169px"/>
|
||||
Their exclusion regions.
|
||||
</td>
|
||||
</tr></tbody></table>
|
||||
|
||||
The main complication in the outlined procedure here is determining how sections qualify in terms of being "inside" and "outside" of our shapes. For this, we need to be able to perform point-in-shape detection, for which we'll use a classic algorithm: getting the "crossing number" by using ray casting, and then testing for "insidedness" by applying the [even-odd rule](http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html): For any point and any shape, we can cast a ray from our point, to some point that we know lies outside of the shape (such as a corner of our drawing surface). We then count how many times that line crosses our shape (remember that we can perform line/curve intersection detection quite easily). If the number of times it crosses the shape's outline is even, the point did not actually lie inside our shape. If the number of intersections is odd, our point did lie inside out shape. With that knowledge, we can decide whether to treat a section that such a point lies on "needs removal" (under union rules), "needs preserving" (under intersection rules), or "needs flipping" (under exclusion rules).
|
||||
|
||||
These operations are expensive, and implementing your own code for this is generally a bad idea if there is already a geometry package available for your language of choice. In this case, for JavaScript the most excellent [Paper.js](http://paperjs.org) already comes with all the code in place to perform efficient boolean shape operations, so rather that implement an inferior version here, I can strongly recommend the Paper.js library if you intend to do any boolean shape work.
|
||||
|
||||
The following graphic shows Paper.js doing its thing for two shapes: one static, and one that is linked to your mouse pointer. If you move the mouse around, you'll see how the shape intersections are resolved. The base shapes are outlined in blue, and the boolean result is coloured red.
|
||||
|
||||
<Graphic preset="simple" title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}><br/>{
|
||||
this.modes.map(mode => {
|
||||
var className = (this.state.mode === mode) ? "selected" : null;
|
||||
return <button className={className} key={mode} onClick={() => this.setMode(mode)}>{mode}</button>;
|
||||
})
|
||||
}</Graphic>
|
@@ -1,17 +1,20 @@
|
||||
var React = require("react");
|
||||
var Graphic = require("../../Graphic.jsx");
|
||||
var SectionHeader = require("../../SectionHeader.jsx");
|
||||
|
||||
var modes = ["unite","intersect","exclude","subtract"];
|
||||
var Locale = require("../../../lib/locale");
|
||||
var locale = new Locale();
|
||||
var page = "shapes";
|
||||
|
||||
var modes;
|
||||
|
||||
var Shapes = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Boolean shape operations"
|
||||
title: locale.getTitle(page)
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
modes = this.modes = ["unite","intersect","exclude","subtract"];
|
||||
return {
|
||||
mode: modes[0]
|
||||
};
|
||||
@@ -97,100 +100,7 @@ var Shapes = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>
|
||||
<SectionHeader {...this.props} />
|
||||
|
||||
<p>We can apply the topics covered so far in this primer to effect boolean shape operations:
|
||||
getting the union, intersection, or exclusion, between two or more shapes that involve Bézier
|
||||
curves. For simplicity (well.. sort of, more homogeneity), we'll be looking at Poly-Bézier
|
||||
shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a
|
||||
simplification (although it does mean we need to write a definition for the class of shapes
|
||||
that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using
|
||||
those in the following examples)</p>
|
||||
|
||||
<p>The procedure for performing boolean operations consists, broadly, of four steps:</p>
|
||||
|
||||
<ol>
|
||||
<li>Find the intersection points between both shapes,</li>
|
||||
<li>cut up the shapes into multiple sections between these intersections,</li>
|
||||
<li>discard any section that isn't part of the desired operation's resultant shape, and</li>
|
||||
<li>link up the remaining sections to form the new shape.</li>
|
||||
</ol>
|
||||
|
||||
<p>Finding all intersections between two poly-Bézier curves, or any poly-line-section shape,
|
||||
is similar to the iterative algorithm discussed in the section on curve/curve intersection.
|
||||
For each segment in the poly-Bézier curve we check whether its bounding box overlaps with
|
||||
any of the segment bounding boxes in the other poly-Bézier curve. If so, we run normal
|
||||
intersection detection.</p>
|
||||
|
||||
<p>After we found all intersection points, we split up our poly-Bézier curves, making sure to
|
||||
record which of the newly formed poly-Bézier curves might potentially link up at the points
|
||||
we split the originals up at. This will let us quickly glue poly-Bézier curves back together
|
||||
after the next step.</p>
|
||||
|
||||
<p>Once we have all the new poly-Bézier curves, we run the first step of the desired boolean
|
||||
operation.</p>
|
||||
|
||||
<ul>
|
||||
<li>Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if
|
||||
we want the union of two overlapping circles, the resulting shape is the outline.</li>
|
||||
<li>Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the
|
||||
two shapes. E.g. if we want the intersection of two overlapping circles, the resulting
|
||||
shape is the tapered ellipse where they overlap.</li>
|
||||
<li>Exclusion: none of the sections are discarded, but we will need to link the shapes back
|
||||
up in a special way. Flip any section that would qualify for removal under UNION rules.</li>
|
||||
</ul>
|
||||
|
||||
<table className="sketch"><tbody><tr><td className="labeled-image">
|
||||
<img src="images/op_base.gif" height="169px"/>
|
||||
<p>Two overlapping shapes.</p>
|
||||
</td><td className="labeled-image">
|
||||
<img src="images/op_union.gif" height="169px"/>
|
||||
<p>The unified region.</p>
|
||||
</td><td className="labeled-image">
|
||||
<img src="images/op_intersection.gif" height="169px"/>
|
||||
<p>Their intersection.</p>
|
||||
</td><td className="labeled-image">
|
||||
<img src="images/op_exclusion.gif" height="169px"/>
|
||||
<p>Their exclusion regions.</p>
|
||||
</td></tr></tbody></table>
|
||||
|
||||
<p>The main complication in the outlined procedure here is determining how sections qualify
|
||||
in terms of being "inside" and "outside" of our shapes. For this, we need to be able to
|
||||
perform point-in-shape detection, for which we'll use a classic algorithm: getting the
|
||||
"crossing number" by using ray casting, and then testing for "insidedness" by applying
|
||||
the <a href="http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html">even-odd
|
||||
rule</a>: For any point and any shape, we can cast a ray from our point, to some point that we know
|
||||
lies outside of the shape (such as a corner of our drawing surface). We then count how many
|
||||
times that line crosses our shape (remember that we can perform line/curve intersection
|
||||
detection quite easily). If the number of times it crosses the shape's outline is even,
|
||||
the point did not actually lie inside our shape. If the number of intersections is odd,
|
||||
our point did lie inside out shape. With that knowledge, we can decide whether to treat
|
||||
a section that such a point lies on "needs removal" (under union rules), "needs preserving"
|
||||
(under intersection rules), or "needs flipping" (under exclusion rules).</p>
|
||||
|
||||
<p>These operations are expensive, and implementing your own code for this is generally
|
||||
a bad idea if there is already a geometry package available for your language of choice.
|
||||
In this case, for JavaScript the most excellent <a href="http://paperjs.org">Paper.js</a> already
|
||||
comes with all the code in place to perform efficient boolean shape operations, so rather
|
||||
that implement an inferior version here, I can strongly recommend the Paper.js library
|
||||
if you intend to do any boolean shape work.</p>
|
||||
|
||||
<p>The following graphic shows Paper.js doing its thing for two shapes: one static, and
|
||||
one that is linked to your mouse pointer. If you move the mouse around, you'll see how
|
||||
the shape intersections are resolved. The base shapes are outlined in blue, and the
|
||||
boolean result is coloured red.</p>
|
||||
|
||||
<Graphic preset="simple" title="Boolean shape operations with Paper.js" paperjs={true}
|
||||
setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}>
|
||||
<br/>{modes.map(mode => {
|
||||
var className = (this.state.mode === mode) ? "selected" : null;
|
||||
return <button className={className} key={mode} onClick={function() { this.setMode(mode); }.bind(this)}>{mode}</button>;
|
||||
})}
|
||||
</Graphic>
|
||||
</section>
|
||||
);
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -82,7 +82,7 @@ var TightBounds = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -139,7 +139,7 @@ var Tracing = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <section>{ locale.getContent(page, this) }</section>;
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -116,9 +116,7 @@ var Whatis = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section>{ locale.getContent(page, this) }</section>
|
||||
);
|
||||
return locale.getContent(page, this);
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user