1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-30 11:40:27 +02:00
This commit is contained in:
Pomax
2020-09-05 22:50:12 -07:00
parent bec07e3297
commit 9434a71d34
46 changed files with 1795 additions and 1623 deletions

View File

@@ -0,0 +1,101 @@
// setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown}
let curve, utils = Bezier.getUtils();
setup() {
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `error`, 0.5);
}
draw() {
clear();
curve.drawSkeleton();
curve.drawCurve();
setColor(`#FF000040`);
let a = this.getArc(curve);
arc(
a.x, a.y, a.r, a.s, a.e,
// draw a wedge, not just the arc
a.x, a.y
);
setColor("black");
text(`Arc approximation with total error ${this.error}`, this.width/2, 15, CENTER);
curve.drawPoints();
}
getArc(curve) {
let ts = 0,
te = 1,
tm = te,
safety = 0,
np1 = curve.get(ts), np2, np3,
arc,
currGood = false,
prevGood = false,
done,
prev_e = 1,
step = 0;
// Find where the good/bad boundary is
te = 1;
// step 2: find the best possible arc
do {
prevGood = currGood;
tm = (ts + te) / 2;
step++;
np2 = curve.get(tm);
np3 = curve.get(te);
arc = utils.getccenter(np1, np2, np3);
arc.interval = { start: ts, end: te, };
let error = this.computeError(arc, np1, ts, te);
currGood = (error <= this.error);
done = prevGood && !currGood;
if (!done) prev_e = te;
// this arc is fine: try a wider arc
if (currGood) {
// if e is already at max, then we're done for this arc.
if (te >= 1) {
// make sure we cap at t=1
arc.interval.end = prev_e = 1;
// if we capped the arc segment to t=1 we also need to make sure that
// the arc's end angle is correct with respect to the bezier end point.
if (te > 1) {
let d = {
x: arc.x + arc.r * cos(arc.e),
y: arc.y + arc.r * sin(arc.e),
};
arc.e += utils.angle({ x: arc.x, y: arc.y }, d, curve.points[3]);
}
done = true;
break;
}
// if not, move it up by half the iteration distance
te = te + (te - ts) / 2;
}
// This is a bad arc: we need to move 'e' down to find a good arc
else { te = tm; }
} while (!done && safety++ < 100);
return arc;
}
computeError(pc, np1, s, e) {
const q = (e - s) / 4,
c1 = curve.get(s + q),
c2 = curve.get(e - q),
ref = dist(pc.x, pc.y, np1.x, np1.y),
d1 = dist(pc.x, pc.y, c1.x, c1.y),
d2 = dist(pc.x, pc.y, c2.x, c2.y);
return abs(d1 - ref) + abs(d2 - ref);
}

View File

@@ -0,0 +1,30 @@
// setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown}
let curve, utils = Bezier.getUtils();
setup() {
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `error`, 0.5);
}
draw() {
clear();
curve.drawSkeleton();
curve.drawCurve();
// See "arc.js" for the code required to find arcs on the curve.
let arcs = curve.arcs(this.error);
arcs.forEach(a => {
setColor( randomColor(0.3) );
arc(
a.x, a.y, a.r, a.s, a.e,
a.x, a.y
);
});
setColor("black");
text(`Arc approximation with total error ${this.error}`, this.width/2, 15, CENTER);
curve.drawPoints();
}

View File

@@ -6,44 +6,40 @@ We already saw in the section on circle approximation that this will never yield
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.
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 [chord](https://en.wikipedia.org/wiki/Chord_%28geometry%29), 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.
We already saw how to fit a circle through three points in the section on [creating a curve from three points](#pointcurves), and 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, to the end point, over the remaining point.
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.
So, how can we convert a Bézier curve into a (sequence of) circular arc(s)?
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!
<Graphic title="Finding a circle through three points" setup={this.setupCircle} draw={this.drawCircle} />
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.
So how can we convert a Bézier curve into a (sequence of) circular arc(s)?
- Start at <em>t=0</em>
- Pick two points further down the curve at some value <em>m = t + n</em> and <em>e = t + 2n</em>
- Start at `t=0`
- Pick two points further down the curve at some value `m = t + n` and `e = t + 2n`
- Find the arc that these points define
- Determine how close the found arc is to the curve:
- Pick two additional points <em>e1 = t + n/2</em> and <em>e2 = t + n + n/2</em>.
- Pick two additional points `e1 = t + n/2` and `e2 = t + n + n/2`.
- 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
lie `on` 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.
- 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.
`actual` distance from the center of the circle to the point on the curve.
- If this error is too high, we consider the arc bad, and try a smaller interval.
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 [Binary Search](https://en.wikipedia.org/wiki/Binary_search_algorithm).
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 `t=0.5`, and then we start performing a [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm).
1. We start with {0, 0.5, 1}
2. That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
- If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
- However, if the arc was still bad, we move <em>down</em> by half the distance: {0, 0.125, 0.25}.
3. 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.
1. We start with `low=0`, `mid=0.5` and `high=1`
2. That'll fail, so we retry with the interval halved: `{0, 0.25, 0.5}`
- If that arc's good, we move back up by half distance: `{0, 0.375, 0.75}`.
- However, if the arc was still bad, we move _down_ by half the distance: `{0, 0.125, 0.25}`.
3. We keep doing this over and over until we have two arcs, 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 good arc.
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 up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is.
<Graphic title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} />
<graphics-element title="First arc approximation of a Bézier curve" src="./arc.js">
<input type="range" min="0.1" max="5" step="0.1" value="0.5" class="slide-control">
</graphics-element>
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 up and down arrow 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:
<Graphic title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawArcs} onKeyDown={this.props.onKeyDown} />
<graphics-element title="Arc approximation of a Bézier curve" src="./arcs.js">
<input type="range" min="0.1" max="5" step="0.1" value="0.5" class="slide-control">
</graphics-element>
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 straightforward, 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 between 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!

View File

@@ -1,184 +0,0 @@
var atan2 = Math.atan2, PI = Math.PI, TAU = 2*PI, cos = Math.cos, sin = Math.sin;
module.exports = {
// These are functions that can be called "From the page",
// rather than being internal to the sketch. This is useful
// for making on-page controls hook into the sketch code.
statics: {
keyHandlingOptions: {
propName: "error",
values: {
"38": 0.1, // up arrow
"40": -0.1 // down arrow
},
controller: function(api) {
if (api.error < 0.1) {
api.error = 0.1;
}
}
}
},
/**
* Setup up a skeleton curve that, when using its
* points for a B-spline, can form a circle.
*/
setupCircle: function(api) {
var curve = new api.Bezier(70,70, 140,40, 240,130);
api.setCurve(curve);
},
/**
* Set up the default quadratic curve.
*/
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
/**
* Set up the default cubic curve.
*/
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.error = 0.5;
},
/**
* Given three points, find the (only!) circle
* that passes through all three points, based
* on the fact that the perpendiculars of the
* chords between the points all cross each
* other at the center of that circle.
*/
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 || m>e) { s += TAU; }
if (s>e) { __=e; e=s; s=__; }
} else {
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;
},
/**
* Draw the circle-computation sketch
*/
drawCircle: function(api, curve) {
api.reset();
var pts = curve.points;
// get center
var C = this.getCCenter(api, pts[0], pts[1], pts[2]);
// outer circle
api.setColor("grey");
api.drawCircle(C, api.utils.dist(C,pts[0]));
// controllable points
api.setColor("black");
pts.forEach(p => api.drawCircle(p,3));
// chords and perpendicular lines
var m;
api.setColor("blue");
api.drawLine(pts[0], pts[1]);
m = {x: (pts[0].x + pts[1].x)/2, y: (pts[0].y + pts[1].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
api.setColor("red");
api.drawLine(pts[1], pts[2]);
m = {x: (pts[1].x + pts[2].x)/2, y: (pts[1].y + pts[2].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
api.setColor("green");
api.drawLine(pts[2], pts[0]);
m = {x: (pts[2].x + pts[0].x)/2, y: (pts[2].y + pts[0].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
// center
api.setColor("black");
api.drawPoint(C);
api.setFill("black");
api.text("Intersection point", C, {x:-25, y:10});
},
/**
* Draw a single arc being fit to a Bezier curve,
* to show off the general application.
*/
drawSingleArc: function(api, curve) {
api.reset();
var arcs = curve.arcs(api.error);
api.drawSkeleton(curve);
api.drawCurve(curve);
var a = arcs[0];
api.setColor("red");
api.setFill("rgba(255,0,0,0.2)");
api.debug = true;
api.drawArc(a);
api.setFill("black");
api.text("Arc approximation with total error " + api.utils.round(api.error,1), {x:10, y:15});
},
/**
* Draw an arc approximation for an entire Bezier curve.
*/
drawArcs: function(api, curve) {
api.reset();
var arcs = curve.arcs(api.error);
api.drawSkeleton(curve);
api.drawCurve(curve);
arcs.forEach(a => {
api.setRandomColor(0.3);
api.setFill(api.getColor());
api.drawArc(a);
});
api.setFill("black");
api.text("Arc approximation with total error " + api.utils.round(api.error,1) + " per arc segment", {x:10, y:15});
}
};