mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-29 19:20:39 +02:00
projection, half moulding
This commit is contained in:
@@ -1,16 +1,27 @@
|
||||
# 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?
|
||||
Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?
|
||||
|
||||
If the Bézier curve is of low enough order, we might be able to [work out the maths for how to do this](https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/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:
|
||||
If the Bézier curve is of low enough order, we might be able to [work out the maths for how to do this](https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/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:
|
||||
|
||||
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.
|
||||
```
|
||||
p = some point to project onto the curve
|
||||
d = some initially huge value
|
||||
i = 0
|
||||
for (coordinate, index) in LUT:
|
||||
if distance(coordinate, p) < d:
|
||||
d = distance(coordinate, p)
|
||||
i = index
|
||||
```
|
||||
|
||||
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.
|
||||
After this runs, we know that `LUT[i]` is the coordinate on the curve _in our LUT_ that is closest to the point we want to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that LUT[i] is a better guess than both LUT[i-1] and LUT[i+1], but there might be an even better projection _somewhere else_ between those two values, so that's what we're going to be testing for, using a variation of the binary search.
|
||||
|
||||
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.
|
||||
1. we start with our point `p`, and the `t` values `t1=LUT[i-1].t` and `t2=LUT[i+1].t`, which span an interval `v = t2-t1`.
|
||||
2. we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and start/end points
|
||||
3. we then check which of these five points is the closest to our original point `p`, and then repeat step 1 with the points before and after the closest point we just found.
|
||||
|
||||
<Graphic title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes so small as to lead to distances that are, for all intents and purposes, the same for all points.
|
||||
|
||||
So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bezier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.
|
||||
|
||||
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./project.js"></graphics-element>
|
||||
|
@@ -1,55 +0,0 @@
|
||||
module.exports = {
|
||||
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.setFill("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);
|
||||
api.text("t = "+api.utils.round(t,2), p, {x:10, y:3});
|
||||
}
|
||||
},
|
||||
|
||||
onMouseMove: function(evt, api) {
|
||||
api.mousePt = {x: evt.offsetX, y: evt.offsetY };
|
||||
api._lut = api.curve.getLUT();
|
||||
}
|
||||
};
|
116
docs/chapters/projections/project.js
Normal file
116
docs/chapters/projections/project.js
Normal file
@@ -0,0 +1,116 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
curve = new Bezier(this, [
|
||||
{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}
|
||||
]);
|
||||
|
||||
this.cursor.x = 280;
|
||||
this.cursor.y = 265
|
||||
|
||||
setMovable(curve.points);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
curve.drawSkeleton(`lightblue`);
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
if (this.currentPoint) return;
|
||||
|
||||
const x = this.cursor.x,
|
||||
y = this.cursor.y,
|
||||
LUT = curve.getLUT(20),
|
||||
i = this.findClosest(x, y, LUT);
|
||||
|
||||
this.showCandidateInterval(x, y, LUT, i);
|
||||
this.drawProjection(x, y, LUT, i);
|
||||
}
|
||||
|
||||
findClosest(x, y, LUT, distance = Number.MAX_SAFE_INTEGER) {
|
||||
let i = 0;
|
||||
LUT.forEach((p, index) => {
|
||||
p.t = index/(LUT.length-1);
|
||||
p.distance = dist(x, y, p.x, p.y);
|
||||
if (p.distance < distance) {
|
||||
distance = p.distance;
|
||||
i = index;
|
||||
}
|
||||
});
|
||||
return i;
|
||||
}
|
||||
|
||||
showCandidateInterval(x, y, LUT, i) {
|
||||
let c = LUT[i];
|
||||
setColor(`rgba(100,255,100)`);
|
||||
circle(c.x, c.y, 3);
|
||||
line(c.x, c.y, x, y);
|
||||
if (i>0) { c = LUT[i-1]; circle(c.x, c.y, 3); line(c.x, c.y, x, y); }
|
||||
if (i<LUT.length-1) { c = LUT[i+1]; circle(c.x, c.y, 3); line(c.x, c.y, x, y); }
|
||||
c = LUT[i];
|
||||
}
|
||||
|
||||
drawProjection(x, y, LUT, i) {
|
||||
let B = this.refineBinary(x, y, LUT, i);
|
||||
setColor(`rgba(100,100,255)`);
|
||||
circle(B.x, B.y, 3);
|
||||
line(B.x, B.y, x, y);
|
||||
}
|
||||
|
||||
/*
|
||||
We already know that LUT[i1] and LUT[i2] are *not* good distances,
|
||||
so we know that a better distance will be somewhere between them.
|
||||
We generate three new points between those two, so we end up with
|
||||
five points, and then check which three of those five are a new,
|
||||
better, interval to check within.
|
||||
*/
|
||||
refineBinary(x, y, LUT, i) {
|
||||
let q, count=1, distance = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
do {
|
||||
let i1 = i === 0 ? 0 : i-1,
|
||||
i2 = i === LUT.length - 1 ? LUT.length -1 : i+1,
|
||||
t1 = LUT[i1].t,
|
||||
t2 = LUT[i2].t,
|
||||
lut = [],
|
||||
step = (t2 - t1)/5;
|
||||
|
||||
if (step < 0.001) break;
|
||||
|
||||
lut.push(LUT[i1]);
|
||||
for(let j=1; j<=3; j++) {
|
||||
let n = curve.get(t1 + j *step);
|
||||
n.distance = dist(n.x, n.y, x, y);
|
||||
if (n.distance < distance) {
|
||||
distance = n.distance;
|
||||
q = n;
|
||||
i = j;
|
||||
}
|
||||
lut.push(n);
|
||||
}
|
||||
lut.push(LUT[i2]);
|
||||
|
||||
// update the LUT to be our new five point LUT, and run again.
|
||||
LUT = lut;
|
||||
|
||||
// The "count" test is mostly a safety measure: it will
|
||||
// never kick in, but something that _will_ terminate is
|
||||
// always better than while(true). Never use while(true)
|
||||
} while (count++ < 25);
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
onMouseMove() {
|
||||
redraw();
|
||||
}
|
Reference in New Issue
Block a user