projection, half moulding
@@ -7,7 +7,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
curve.drawSkeleton();
|
||||
curve.drawCurve();
|
||||
|
@@ -4,7 +4,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
if (this.canonicalMap) {
|
||||
|
@@ -15,7 +15,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
const w = this.width/2,
|
||||
|
@@ -12,7 +12,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
const dim = this.height;
|
||||
curve.drawSkeleton();
|
||||
|
@@ -44,7 +44,6 @@ binomial(n,k) {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
setFill(`black`);
|
||||
setStroke(`black`);
|
||||
|
@@ -37,7 +37,6 @@ setupEventListening() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
// panel 1: base curves
|
||||
|
@@ -12,7 +12,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
const dim = this.height;
|
||||
const degree = curve.points.length - 1;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the <em>t</em>-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.
|
||||
|
||||
<Graphic title="Moulding a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markQB} onMouseDrag={this.dragQB} onMouseUp={this.saveCurve}/>
|
||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" src="./mould-quadratic.js"></graphics-element>
|
||||
|
||||
**Click-dragging the curve itself** shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding <i>t</i>-value, the original point C for that <i>t</i>-value, as well as the new point B' based on the mouse cursor. Since we know the <i>t</i>-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:
|
||||
|
||||
|
110
docs/chapters/moulding/mould-quadratic.js
Normal file
@@ -0,0 +1,110 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
setPanelCount(3);
|
||||
curve = Bezier.defaultQuadratic(this);
|
||||
this.position = {x:0,y:0};
|
||||
setMovable(curve.points, [this.position]);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
|
||||
curve.drawSkeleton();
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
if (this.position) {
|
||||
setColor(`blue`);
|
||||
let p = this.position.projection;
|
||||
if (!this.mark) {
|
||||
p = this.position.projection = curve.project(
|
||||
this.position.x,
|
||||
this.position.y
|
||||
)
|
||||
this.position.x = p.x;
|
||||
this.position.y = p.y;
|
||||
}
|
||||
circle(p.x, p.y, 3);
|
||||
}
|
||||
|
||||
nextPanel();
|
||||
setStroke(`black`);
|
||||
line(0,0,0,this.height);
|
||||
|
||||
curve.drawSkeleton(`lightblue`);
|
||||
curve.drawCurve(`lightblue`);
|
||||
curve.points.forEach(p => circle(p.x, p.y, 2));
|
||||
|
||||
if (this.mark) {
|
||||
let B = this.mark.B;
|
||||
setFill(`black`);
|
||||
text(`t = ${this.mark.t.toFixed(2)}`, B.x + 5, B.y + 10);
|
||||
|
||||
let {A, C, S, E} = curve.getABC(this.mark.t, B);
|
||||
setColor(`lightblue`);
|
||||
line(S.x, S.y, E.x, E.y);
|
||||
line(A.x, A.y, C.x, C.y);
|
||||
circle(A.x, A.y, 3);
|
||||
circle(B.x, B.y, 3);
|
||||
circle(C.x, C.y, 3);
|
||||
|
||||
if (this.currentPoint) {
|
||||
let {A,B,C,S,E} = curve.getABC(this.mark.t, this.position);
|
||||
setColor(`purple`);
|
||||
line(A.x, A.y, C.x, C.y);
|
||||
line(S.x, S.y, A.x, A.y);
|
||||
line(E.x, E.y, A.x, A.y);
|
||||
circle(A.x, A.y, 3);
|
||||
circle(B.x, B.y, 3);
|
||||
circle(C.x, C.y, 3);
|
||||
|
||||
this.moulded = new Bezier(this, [S,A,E]);
|
||||
}
|
||||
}
|
||||
|
||||
nextPanel();
|
||||
setStroke(`black`);
|
||||
line(0,0,0,this.height);
|
||||
|
||||
if (this.moulded) {
|
||||
this.moulded.drawSkeleton(`lightblue`);
|
||||
this.moulded.drawCurve(`black`);
|
||||
this.moulded.points.forEach(p => circle(p.x, p.y, 2));
|
||||
} else {
|
||||
curve.drawSkeleton(`lightblue`);
|
||||
curve.drawCurve(`black`);
|
||||
curve.points.forEach(p => circle(p.x, p.y, 2));
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
if (this.currentPoint !== this.position) {
|
||||
this.mark = false;
|
||||
this.position.projection = false;
|
||||
}
|
||||
else if (this.position.projection) {
|
||||
this.mark = {
|
||||
B: this.position.projection,
|
||||
t: this.position.projection.t
|
||||
};
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
onMouseMove() {
|
||||
if (!this.currentPoint && !this.mark) {
|
||||
this.position.x = this.cursor.x;
|
||||
this.position.y = this.cursor.y;
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
this.mark = false;
|
||||
if (this.moulded) {
|
||||
curve = this.moulded;
|
||||
resetMovable(curve.points, [this.position]);
|
||||
}
|
||||
redraw();
|
||||
}
|
@@ -29,7 +29,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
translate(this.width/2 - 60, this.height/2 + 75);
|
||||
const curve = this.curve;
|
||||
|
@@ -29,7 +29,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
translate(this.width/2 - 60, this.height/2 + 75);
|
||||
const curve = this.curve;
|
||||
|
@@ -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
@@ -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();
|
||||
}
|
@@ -7,7 +7,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
let p = curve.get(this.position);
|
||||
|
@@ -45,6 +45,7 @@ export default [
|
||||
|
||||
// curve manipulation
|
||||
'abc',
|
||||
'projections',
|
||||
'moulding',
|
||||
'pointcurves',
|
||||
'curvefitting',
|
||||
@@ -59,7 +60,6 @@ export default [
|
||||
// 'drawing', // still just waiting to be finished......
|
||||
|
||||
// curve offsetting
|
||||
'projections',
|
||||
'offsetting',
|
||||
'graduatedoffset',
|
||||
|
||||
|
@@ -6,7 +6,6 @@ setup(api) {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
curve.drawSkeleton();
|
||||
|
@@ -7,7 +7,6 @@ setup(api) {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
curve.drawSkeleton();
|
||||
|
@@ -7,7 +7,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear(`white`);
|
||||
this.drawBasics();
|
||||
this.drawPointCurve();
|
||||
@@ -95,7 +94,7 @@ drawOnCurve(np4, i) {
|
||||
}
|
||||
|
||||
drawCurveCoordinates() {
|
||||
this.resetTransform();
|
||||
resetTransform();
|
||||
curve.drawPoints();
|
||||
translate(this.height, 0);
|
||||
curve.drawPoints();
|
||||
|
@@ -7,7 +7,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
curve.drawSkeleton();
|
||||
|
@@ -7,7 +7,6 @@ setup() {
|
||||
}
|
||||
|
||||
draw() {
|
||||
resetTransform();
|
||||
clear();
|
||||
|
||||
curve.drawSkeleton();
|
||||
|
Before Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
BIN
docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 18 KiB |
BIN
docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png
Normal file
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 11 KiB |
BIN
docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png
Normal file
After Width: | Height: | Size: 12 KiB |
@@ -95,6 +95,7 @@
|
||||
<li><a href="#intersections">Intersections</a></li>
|
||||
<li><a href="#curveintersection">Curve/curve intersection</a></li>
|
||||
<li><a href="#abc">The projection identity</a></li>
|
||||
<li><a href="#projections">Projecting a point onto a Bézier curve</a></li>
|
||||
<li><a href="#moulding">Manipulating a curve</a></li>
|
||||
<li><a href="#pointcurves">Creating a curve from three points</a></li>
|
||||
<li><a href="#curvefitting">Curve fitting</a></li>
|
||||
@@ -102,7 +103,6 @@
|
||||
<li><a href="#catmullmoulding">Creating a Catmull-Rom curve from three points</a></li>
|
||||
<li><a href="#polybezier">Forming poly-Bézier curves</a></li>
|
||||
<li><a href="#shapes">Boolean shape operations</a></li>
|
||||
<li><a href="#projections">Projecting a point onto a Bézier curve</a></li>
|
||||
<li><a href="#offsetting">Curve offsetting</a></li>
|
||||
<li><a href="#graduatedoffset">Graduated curve offsetting</a></li>
|
||||
<li><a href="#circles">Circles and quadratic Bézier curves</a></li>
|
||||
@@ -219,7 +219,7 @@
|
||||
<p>So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over those lines gives us two points, between which we can again perform linear interpolation, yielding a single point. And that point —and all points we can form in this way for all ratios taken together— form our Bézier curve:</p>
|
||||
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" height="275" src="./chapters/whatis/interpolation.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\d3d0579ce3534dcee1fda11bdf609c22.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\9b3633889c38325c24c19ce18ab94ad6.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
|
||||
@@ -328,7 +328,7 @@ function Bezier(3,t):
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic interpolations" width="275" height="275" src="./chapters/control/lerp.js" data-degree="3">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\62d7ef11f60acde82868424364a477e8.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\ecc15848fbe7b2176b0c89973f07c694.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -336,7 +336,7 @@ function Bezier(3,t):
|
||||
|
||||
<graphics-element title="Cubic interpolations" width="275" height="275" src="./chapters/control/lerp.js" data-degree="4">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\9331dd83c72b233190eaca1cfcc169db.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\49423783987ac4bc49fbe4c519dbc1d1.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -344,7 +344,7 @@ function Bezier(3,t):
|
||||
|
||||
<graphics-element title="15th degree interpolations" width="275" height="275" src="./chapters/control/lerp.js" data-degree="15">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\44242f6a6be718bea46292369d509520.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\afd21a9ba16965c2e7ec2d0d14892250.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -604,7 +604,7 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>Using de Casteljau's algorithm, we can also find all the points we need to split up a Bézier curve into two, smaller curves, which taken together form the original curve. When we construct de Casteljau's skeleton for some value <code>t</code>, the procedure gives us all the points we need to split a curve at that <code>t</code> value: one curve is defined by all the inside skeleton points found prior to our on-curve point, with the other curve being defined by all the inside skeleton points after our on-curve point.</p>
|
||||
<graphics-element title="Splitting a curve" width="825" height="275" src="./chapters/splitting/splitting.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\43f3024045439e3bcc95434fa39f9d03.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\bef2f09698c0d3d2b7c4c031be17ff69.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
@@ -836,7 +836,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/frenet.js" >
|
||||
<fallback-image>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\ca775688322e0da5c5ecc5a15e2e57de.png" loading="lazy">
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\cbadec403b99dec015ab084ee10e1671.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -910,7 +910,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/rotation-minimizing.js" >
|
||||
<fallback-image>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\f989c02a1c37d8837e1e50e993415a73.png" loading="lazy">
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\9983328e50c2156c124bb1ea4ad5c05b.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -928,13 +928,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.</p>
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\components\f7490a1c523d4dc8772b621b4a61fdd4.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\components\008604bc4c53bd7e0d97c99a67812ad1.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\components\35d69b33228ae64221385047177b67a5.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\components\5214256129e6396e7ac1f1713fa9c88d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1070,14 +1070,14 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:</p>
|
||||
<graphics-element title="Quadratic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\6d246f1c53e40bd5156ef50c4046db51.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\9850eec01924deae7fda4400ce44270d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||||
<graphics-element title="Cubic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\05ff4df8b73ba25dffeb42f768e0e9c4.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\890406c7bc96904224f8f14940bf3e56.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1123,12 +1123,12 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\2bac71234aed2bca2686fdbce7dd78d8.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\b3ccd45a72c815388aee6515fe37a486.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\ed3e2ad3961fbf9e9e5bc951f1d79302.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\31655f24b7dd8b8871687b6610d9ac0e.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
@@ -1198,7 +1198,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
<graphics-element title="The canonical curve map" width="400" height="400" src="./chapters/canonical/canonical.js" >
|
||||
<fallback-image>
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\0611789dcef56f4dc544ac806eada228.png" loading="lazy">
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\675217e6df3d75c86503dc2af623e14e.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1264,7 +1264,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:</p>
|
||||
<graphics-element title="A cubic curve mapped to canonical form" width="800" height="400" src="./chapters/canonical/interactive.js" >
|
||||
<fallback-image>
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\3efa5cf9f4bef7b5a656a118a17e5c7b.png" loading="lazy">
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\344346f09b6d5d7e3ddf91084ad50d46.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1275,7 +1275,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\a6f705eb306c43e5709970b2ccad9d20.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\dd28d64458d22f4fe89c98568258efcb.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -1302,7 +1302,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So the procedure is fairly straight forward: pick an <code>x</code>, find the associted <code>t</code> value, evaluate our curve <em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this <code>x</code>. Move the slider for the following graphic to see this in action:</p>
|
||||
<graphics-element title="Finding By(t), by finding t for a given x" width="275" height="275" src="./chapters/yforx/yforx.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\e1549cca3a5203c4e4d7fa22948cb3f8.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\efcfe9b48ca4e65eef3d4bf3e4c97bc3.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -1449,7 +1449,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\4f2cd306ec6fa0340ac7f410744b3118.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\52f815cefe99dabc47ca83d0b97b61fc.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1457,7 +1457,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\25e9697557129c651e9c7cc4e4878b16.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\133bf9d02801a3149c9ddb8b313e6797.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
@@ -1540,7 +1540,7 @@ lli = function(line1, line2):
|
||||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||||
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\a71619a14589851390cf88aa07042d3e.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\02d70e27ba678db49a883afcc6264c9a.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
@@ -1600,22 +1600,52 @@ lli = function(line1, line2):
|
||||
<img class="LaTeX SVG" src="./images/chapters/abc/12aaf0d7fd20b3c551a0ec76b18bd7d2.svg" width="231px" height="39px" loading="lazy">
|
||||
<p>So: if we have a curve's start and end point, then for any <code>t</code> value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.</p>
|
||||
|
||||
</section>
|
||||
<section id="projections">
|
||||
<h1><a href="#projections">Projecting a point onto a Bézier curve</a></h1>
|
||||
<p>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?</p>
|
||||
<p>If the Bézier curve is of low enough order, we might be able to <a href="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how to do this</a>, and get a perfect <code>t</code> 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 <code>t</code> 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 <code>t</code> values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:</p>
|
||||
<pre><code>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</code></pre>
|
||||
<p>After this runs, we know that <code>LUT[i]</code> is the coordinate on the curve <em>in our LUT</em> 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 <em>somewhere else</em> between those two values, so that's what we're going to be testing for, using a variation of the binary search.</p>
|
||||
<ol>
|
||||
<li>we start with our point <code>p</code>, and the <code>t</code> values <code>t1=LUT[i-1].t</code> and <code>t2=LUT[i+1].t</code>, which span an interval <code>v = t2-t1</code>.</li>
|
||||
<li>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</li>
|
||||
<li>we then check which of these five points is the closest to our original point <code>p</code>, and then repeat step 1 with the points before and after the closest point we just found.</li>
|
||||
</ol>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./chapters/projections/project.js" >
|
||||
<fallback-image>
|
||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
<section id="moulding">
|
||||
<h1><a href="#moulding">Manipulating a curve</a></h1>
|
||||
<p>Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the <em>t</em>-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.</p>
|
||||
<Graphic title="Moulding a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markQB} onMouseDrag={this.dragQB} onMouseUp={this.saveCurve}/>
|
||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/mould-quadratic.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\moulding\7f080cfc5764282db126164d6705d83d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p><strong>Click-dragging the curve itself</strong> shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding <i>t</i>-value, the original point C for that <i>t</i>-value, as well as the new point B' based on the mouse cursor. Since we know the <i>t</i>-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="221px" height="36px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="239px" height="35px" loading="lazy">
|
||||
<p>For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point. For cubic curves, we need to do a little more work:</p>
|
||||
<Graphic title="Moulding a cubic Bézier curve" setup={this.setupCubic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markCB} onMouseDrag={this.dragCB} onMouseUp={this.saveCurve}/>
|
||||
|
||||
<p>To help understand what's going on, the cubic graphic shows the full de Casteljau construction "hull" when repositioning point B. We compute A' in exactly the same way as before, but we also record the final strut line that forms B in the original curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points should be. Remember that B' lies on line e1--e2 at a distance <i>t</i>, because that's how Bézier curves work. In the same manner, we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing the new control points is fairly easy.</p>
|
||||
<p>First, we construct the one-level-of-de-Casteljau-up points:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/524206c49f317d27d8e07a310b24a7a3.svg" width="132px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/524206c49f317d27d8e07a310b24a7a3.svg" width="139px" height="75px" loading="lazy">
|
||||
<p>And then we can compute the new control points:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg" width="156px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg" width="168px" height="72px" loading="lazy">
|
||||
<p>And that's cubic curve manipulation.</p>
|
||||
|
||||
</section>
|
||||
@@ -1885,21 +1915,6 @@ lli = function(line1, line2):
|
||||
<button onclick="() => this.setMode(mode)">mode</button>
|
||||
</Graphic>
|
||||
|
||||
</section>
|
||||
<section id="projections">
|
||||
<h1><a href="#projections">Projecting a point onto a Bézier curve</a></h1>
|
||||
<p>Say we have a Bézier curve and some point, not on the curve, of which we want to know which <code>t</code> 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="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how to do this</a>, and get a perfect <code>t</code> 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 <code>t</code> 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 <code>t</code> 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 <code>t</code> value we found, start with some small interval around <code>t</code> (1/length_of_LUT on either side is a reasonable start),</li>
|
||||
<li>if the distance to <code>t ± interval/2</code> is larger than the distance to <code>t</code>, try again with the interval reduced to half its original length.</li>
|
||||
<li>if the distance to <code>t ± interval/2</code> is smaller than the distance to <code>t</code>, replace <code>t</code> with the smaller-distance value.</li>
|
||||
<li>after reducing the interval, or changing <code>t</code>, 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 <code>t</code> 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" <code>t</code> value.</p>
|
||||
<Graphic title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
|
||||
</section>
|
||||
<section id="offsetting">
|
||||
<h1><a href="#offsetting">Curve offsetting</a></h1>
|
||||
|
@@ -95,6 +95,7 @@
|
||||
<li><a href="ja-JP/index.html#intersections">Intersections</a></li>
|
||||
<li><a href="ja-JP/index.html#curveintersection">Curve/curve intersection</a></li>
|
||||
<li><a href="ja-JP/index.html#abc">The projection identity</a></li>
|
||||
<li><a href="ja-JP/index.html#projections">Projecting a point onto a Bézier curve</a></li>
|
||||
<li><a href="ja-JP/index.html#moulding">Manipulating a curve</a></li>
|
||||
<li><a href="ja-JP/index.html#pointcurves">Creating a curve from three points</a></li>
|
||||
<li><a href="ja-JP/index.html#curvefitting">Curve fitting</a></li>
|
||||
@@ -102,7 +103,6 @@
|
||||
<li><a href="ja-JP/index.html#catmullmoulding">Creating a Catmull-Rom curve from three points</a></li>
|
||||
<li><a href="ja-JP/index.html#polybezier">Forming poly-Bézier curves</a></li>
|
||||
<li><a href="ja-JP/index.html#shapes">Boolean shape operations</a></li>
|
||||
<li><a href="ja-JP/index.html#projections">Projecting a point onto a Bézier curve</a></li>
|
||||
<li><a href="ja-JP/index.html#offsetting">Curve offsetting</a></li>
|
||||
<li><a href="ja-JP/index.html#graduatedoffset">Graduated curve offsetting</a></li>
|
||||
<li><a href="ja-JP/index.html#circles">Circles and quadratic Bézier curves</a></li>
|
||||
@@ -222,7 +222,7 @@
|
||||
<p>では、実際に見てみましょう。下の図はインタラクティブになっています。上下キーで補間の比率が増減しますので、どうなるか確かめてみましょう。最初に3点があり、それを結んで2本の直線が引かれています。この直線の上でそれぞれ線形補間を行うと、2つの点が得られます。この2点の間でさらに線形補間を行うと、1つの点を得ることができます。そして、あらゆる比率に対して同様に点を求め、それをすべて集めると、このようにベジエ曲線ができるのです。</p>
|
||||
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" height="275" src="./chapters/whatis/interpolation.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\d3d0579ce3534dcee1fda11bdf609c22.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\9b3633889c38325c24c19ce18ab94ad6.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
|
||||
@@ -330,7 +330,7 @@ function Bezier(3,t):
|
||||
<div class="figure">
|
||||
<graphics-element title="2次の補間" width="275" height="275" src="./chapters/control/lerp.js" data-degree="3">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\62d7ef11f60acde82868424364a477e8.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\ecc15848fbe7b2176b0c89973f07c694.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -338,7 +338,7 @@ function Bezier(3,t):
|
||||
|
||||
<graphics-element title="3次の補間" width="275" height="275" src="./chapters/control/lerp.js" data-degree="4">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\9331dd83c72b233190eaca1cfcc169db.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\49423783987ac4bc49fbe4c519dbc1d1.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -346,7 +346,7 @@ function Bezier(3,t):
|
||||
|
||||
<graphics-element title="15次の補間" width="275" height="275" src="./chapters/control/lerp.js" data-degree="15">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\44242f6a6be718bea46292369d509520.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\afd21a9ba16965c2e7ec2d0d14892250.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -601,7 +601,7 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>ベジエ曲線を分割して、繫ぎ合わせたときに元に戻るような小さい2曲線にしたい場合にも、ド・カステリョのアルゴリズムを使えば、これに必要な点をすべて求めることができます。ある値<code>t</code>に対してド・カステリョの骨格を組み立てると、その<code>t</code>で曲線を分割する際に必要になる点がすべて得られます。骨格内部の点のうち、曲線上の点から見て手前側にある点によって一方の曲線が定義され、向こう側にある点によってもう一方の曲線が定義されます。</p>
|
||||
<graphics-element title="曲線の分割" width="825" height="275" src="./chapters/splitting/splitting.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\43f3024045439e3bcc95434fa39f9d03.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\bef2f09698c0d3d2b7c4c031be17ff69.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
@@ -833,7 +833,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/frenet.js" >
|
||||
<fallback-image>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\ca775688322e0da5c5ecc5a15e2e57de.png" loading="lazy">
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\cbadec403b99dec015ab084ee10e1671.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -907,7 +907,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/rotation-minimizing.js" >
|
||||
<fallback-image>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\f989c02a1c37d8837e1e50e993415a73.png" loading="lazy">
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\9983328e50c2156c124bb1ea4ad5c05b.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -925,13 +925,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.</p>
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\components\f7490a1c523d4dc8772b621b4a61fdd4.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\components\008604bc4c53bd7e0d97c99a67812ad1.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\components\35d69b33228ae64221385047177b67a5.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\components\5214256129e6396e7ac1f1713fa9c88d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1067,14 +1067,14 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:</p>
|
||||
<graphics-element title="Quadratic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\6d246f1c53e40bd5156ef50c4046db51.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\9850eec01924deae7fda4400ce44270d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||||
<graphics-element title="Cubic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\05ff4df8b73ba25dffeb42f768e0e9c4.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\890406c7bc96904224f8f14940bf3e56.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1120,12 +1120,12 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\2bac71234aed2bca2686fdbce7dd78d8.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\b3ccd45a72c815388aee6515fe37a486.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\ed3e2ad3961fbf9e9e5bc951f1d79302.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\31655f24b7dd8b8871687b6610d9ac0e.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
@@ -1195,7 +1195,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
<graphics-element title="The canonical curve map" width="400" height="400" src="./chapters/canonical/canonical.js" >
|
||||
<fallback-image>
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\0611789dcef56f4dc544ac806eada228.png" loading="lazy">
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\675217e6df3d75c86503dc2af623e14e.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1261,7 +1261,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:</p>
|
||||
<graphics-element title="A cubic curve mapped to canonical form" width="800" height="400" src="./chapters/canonical/interactive.js" >
|
||||
<fallback-image>
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\3efa5cf9f4bef7b5a656a118a17e5c7b.png" loading="lazy">
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\344346f09b6d5d7e3ddf91084ad50d46.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1272,7 +1272,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\a6f705eb306c43e5709970b2ccad9d20.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\dd28d64458d22f4fe89c98568258efcb.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -1299,7 +1299,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So the procedure is fairly straight forward: pick an <code>x</code>, find the associted <code>t</code> value, evaluate our curve <em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this <code>x</code>. Move the slider for the following graphic to see this in action:</p>
|
||||
<graphics-element title="Finding By(t), by finding t for a given x" width="275" height="275" src="./chapters/yforx/yforx.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\e1549cca3a5203c4e4d7fa22948cb3f8.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\efcfe9b48ca4e65eef3d4bf3e4c97bc3.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -1446,7 +1446,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\4f2cd306ec6fa0340ac7f410744b3118.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\52f815cefe99dabc47ca83d0b97b61fc.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1454,7 +1454,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\25e9697557129c651e9c7cc4e4878b16.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\133bf9d02801a3149c9ddb8b313e6797.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
@@ -1537,7 +1537,7 @@ lli = function(line1, line2):
|
||||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||||
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\a71619a14589851390cf88aa07042d3e.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\02d70e27ba678db49a883afcc6264c9a.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
@@ -1597,22 +1597,52 @@ lli = function(line1, line2):
|
||||
<img class="LaTeX SVG" src="./images/chapters/abc/12aaf0d7fd20b3c551a0ec76b18bd7d2.svg" width="231px" height="39px" loading="lazy">
|
||||
<p>So: if we have a curve's start and end point, then for any <code>t</code> value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.</p>
|
||||
|
||||
</section>
|
||||
<section id="projections">
|
||||
<h1><a href="ja-JP/index.html#projections">Projecting a point onto a Bézier curve</a></h1>
|
||||
<p>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?</p>
|
||||
<p>If the Bézier curve is of low enough order, we might be able to <a href="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how to do this</a>, and get a perfect <code>t</code> 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 <code>t</code> 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 <code>t</code> values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:</p>
|
||||
<pre><code>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</code></pre>
|
||||
<p>After this runs, we know that <code>LUT[i]</code> is the coordinate on the curve <em>in our LUT</em> 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 <em>somewhere else</em> between those two values, so that's what we're going to be testing for, using a variation of the binary search.</p>
|
||||
<ol>
|
||||
<li>we start with our point <code>p</code>, and the <code>t</code> values <code>t1=LUT[i-1].t</code> and <code>t2=LUT[i+1].t</code>, which span an interval <code>v = t2-t1</code>.</li>
|
||||
<li>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</li>
|
||||
<li>we then check which of these five points is the closest to our original point <code>p</code>, and then repeat step 1 with the points before and after the closest point we just found.</li>
|
||||
</ol>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./chapters/projections/project.js" >
|
||||
<fallback-image>
|
||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
<section id="moulding">
|
||||
<h1><a href="ja-JP/index.html#moulding">Manipulating a curve</a></h1>
|
||||
<p>Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the <em>t</em>-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.</p>
|
||||
<Graphic title="Moulding a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markQB} onMouseDrag={this.dragQB} onMouseUp={this.saveCurve}/>
|
||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/mould-quadratic.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\moulding\7f080cfc5764282db126164d6705d83d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p><strong>Click-dragging the curve itself</strong> shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding <i>t</i>-value, the original point C for that <i>t</i>-value, as well as the new point B' based on the mouse cursor. Since we know the <i>t</i>-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="221px" height="36px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="239px" height="35px" loading="lazy">
|
||||
<p>For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point. For cubic curves, we need to do a little more work:</p>
|
||||
<Graphic title="Moulding a cubic Bézier curve" setup={this.setupCubic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markCB} onMouseDrag={this.dragCB} onMouseUp={this.saveCurve}/>
|
||||
|
||||
<p>To help understand what's going on, the cubic graphic shows the full de Casteljau construction "hull" when repositioning point B. We compute A' in exactly the same way as before, but we also record the final strut line that forms B in the original curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points should be. Remember that B' lies on line e1--e2 at a distance <i>t</i>, because that's how Bézier curves work. In the same manner, we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing the new control points is fairly easy.</p>
|
||||
<p>First, we construct the one-level-of-de-Casteljau-up points:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/524206c49f317d27d8e07a310b24a7a3.svg" width="132px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/524206c49f317d27d8e07a310b24a7a3.svg" width="139px" height="75px" loading="lazy">
|
||||
<p>And then we can compute the new control points:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg" width="156px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg" width="168px" height="72px" loading="lazy">
|
||||
<p>And that's cubic curve manipulation.</p>
|
||||
|
||||
</section>
|
||||
@@ -1882,21 +1912,6 @@ lli = function(line1, line2):
|
||||
<button onclick="() => this.setMode(mode)">mode</button>
|
||||
</Graphic>
|
||||
|
||||
</section>
|
||||
<section id="projections">
|
||||
<h1><a href="ja-JP/index.html#projections">Projecting a point onto a Bézier curve</a></h1>
|
||||
<p>Say we have a Bézier curve and some point, not on the curve, of which we want to know which <code>t</code> 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="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how to do this</a>, and get a perfect <code>t</code> 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 <code>t</code> 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 <code>t</code> 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 <code>t</code> value we found, start with some small interval around <code>t</code> (1/length_of_LUT on either side is a reasonable start),</li>
|
||||
<li>if the distance to <code>t ± interval/2</code> is larger than the distance to <code>t</code>, try again with the interval reduced to half its original length.</li>
|
||||
<li>if the distance to <code>t ± interval/2</code> is smaller than the distance to <code>t</code>, replace <code>t</code> with the smaller-distance value.</li>
|
||||
<li>after reducing the interval, or changing <code>t</code>, 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 <code>t</code> 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" <code>t</code> value.</p>
|
||||
<Graphic title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
|
||||
</section>
|
||||
<section id="offsetting">
|
||||
<h1><a href="ja-JP/index.html#offsetting">Curve offsetting</a></h1>
|
||||
|
@@ -436,12 +436,13 @@ class GraphicsAPI extends BaseAPI {
|
||||
/**
|
||||
* Reset the canvas bitmap to a uniform color.
|
||||
*/
|
||||
clear(color = `white`) {
|
||||
clear(color = `white`, preserveTransforms = false) {
|
||||
this.ctx.cacheStyle();
|
||||
this.resetTransform();
|
||||
this.ctx.fillStyle = color;
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.restoreStyle();
|
||||
if (!preserveTransforms) this.resetTransform();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -34,8 +34,11 @@ class Bezier extends Original {
|
||||
this.ctx = apiInstance.ctx;
|
||||
}
|
||||
|
||||
getPointNear(point, d = 5) {
|
||||
const { x, y } = point;
|
||||
project(x, y) {
|
||||
return super.project({ x, y });
|
||||
}
|
||||
|
||||
getPointNear(x, y, d = 5) {
|
||||
const p = this.points;
|
||||
for (let i = 0, e = p.length; i < e; i++) {
|
||||
let dx = Math.abs(p[i].x - x);
|
||||
@@ -46,46 +49,6 @@ class Bezier extends Original {
|
||||
}
|
||||
}
|
||||
|
||||
getProjectionPoint(point) {
|
||||
const { x, y } = point;
|
||||
// project this point onto the curve and return _that_ point
|
||||
const n = this.lut.length - 1,
|
||||
p = this.points;
|
||||
|
||||
let d,
|
||||
closest,
|
||||
smallestDistance = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// coarse check
|
||||
this.lut.forEach((p, i) => {
|
||||
d = p.dist(x, y);
|
||||
if (d < smallestDistance) {
|
||||
smallestDistance = d;
|
||||
p.t = i / n;
|
||||
closest = p;
|
||||
}
|
||||
});
|
||||
|
||||
// fine check
|
||||
for (let o = -0.1, t, np, st = closest.t; o <= 0.1; o += 0.005) {
|
||||
t = st + o;
|
||||
if (t < 0) continue;
|
||||
if (t > 1) continue;
|
||||
np = new Point(
|
||||
compute(t, p[0].x, p[1].x, p[2].x, p[3].x),
|
||||
compute(t, p[0].y, p[1].y, p[2].y, p[3].y)
|
||||
);
|
||||
d = np.dist(x, y);
|
||||
if (d < smallestDistance) {
|
||||
smallestDistance = d;
|
||||
closest = np;
|
||||
closest.t = t;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
drawCurve(color = `#333`) {
|
||||
const ctx = this.ctx;
|
||||
ctx.cacheStyle();
|
||||
|
@@ -32,7 +32,7 @@ function getABC(n, S, B, E, t) {
|
||||
x: B.x + (B.x - C.x) / s,
|
||||
y: B.y + (B.y - C.y) / s,
|
||||
};
|
||||
return { A: A, B: B, C: C };
|
||||
return { A, B, C };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,6 +238,15 @@ class Bezier {
|
||||
return utils.length(this.derivative.bind(this));
|
||||
}
|
||||
|
||||
getABC(t, B) {
|
||||
let S = this.points[0];
|
||||
let E = this.points[this.order];
|
||||
let ret = getABC(this.order, S, B || this.get(t), E, t);
|
||||
ret.S = S;
|
||||
ret.E = E;
|
||||
return ret;
|
||||
}
|
||||
|
||||
getLUT(steps) {
|
||||
this.verify();
|
||||
steps = steps || 100;
|
||||
@@ -248,8 +257,11 @@ class Bezier {
|
||||
// We want a range from 0 to 1 inclusive, so
|
||||
// we decrement and then use <= rather than <:
|
||||
steps--;
|
||||
for (let t = 0; t <= steps; t++) {
|
||||
this._lut.push(this.compute(t / steps));
|
||||
for (let i = 0, p, t; i < steps; i++) {
|
||||
t = i / (steps - 1);
|
||||
p = this.compute(t);
|
||||
p.t = t;
|
||||
this._lut.push(p);
|
||||
}
|
||||
return this._lut;
|
||||
}
|
||||
|
@@ -90,12 +90,14 @@ const utils = {
|
||||
compute: function (t, points, _3d) {
|
||||
// shortcuts
|
||||
if (t === 0) {
|
||||
points[0].t = 0;
|
||||
return points[0];
|
||||
}
|
||||
|
||||
const order = points.length - 1;
|
||||
|
||||
if (t === 1) {
|
||||
points[order].t = 1;
|
||||
return points[order];
|
||||
}
|
||||
|
||||
@@ -104,6 +106,7 @@ const utils = {
|
||||
|
||||
// constant?
|
||||
if (order === 0) {
|
||||
points[0].t = t;
|
||||
return points[0];
|
||||
}
|
||||
|
||||
@@ -112,6 +115,7 @@ const utils = {
|
||||
const ret = {
|
||||
x: mt * p[0].x + t * p[1].x,
|
||||
y: mt * p[0].y + t * p[1].y,
|
||||
t: t,
|
||||
};
|
||||
if (_3d) {
|
||||
ret.z = mt * p[0].z + t * p[1].z;
|
||||
@@ -141,6 +145,7 @@ const utils = {
|
||||
const ret = {
|
||||
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
|
||||
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
|
||||
t: t,
|
||||
};
|
||||
if (_3d) {
|
||||
ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
|
||||
@@ -162,6 +167,7 @@ const utils = {
|
||||
}
|
||||
dCpts.splice(dCpts.length - 1, 1);
|
||||
}
|
||||
dCpts[0].t = t;
|
||||
return dCpts[0];
|
||||
},
|
||||
|
||||
@@ -186,6 +192,7 @@ const utils = {
|
||||
x: (f1 * p[0].x + f2 * p[1].x) / d,
|
||||
y: (f1 * p[0].y + f2 * p[1].y) / d,
|
||||
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d,
|
||||
t: t,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,6 +207,7 @@ const utils = {
|
||||
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d,
|
||||
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d,
|
||||
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d,
|
||||
t: t,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,6 +225,7 @@ const utils = {
|
||||
z: !_3d
|
||||
? false
|
||||
: (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d,
|
||||
t: t,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@@ -95,6 +95,7 @@
|
||||
<li><a href="zh-CN/index.html#intersections">Intersections</a></li>
|
||||
<li><a href="zh-CN/index.html#curveintersection">Curve/curve intersection</a></li>
|
||||
<li><a href="zh-CN/index.html#abc">The projection identity</a></li>
|
||||
<li><a href="zh-CN/index.html#projections">Projecting a point onto a Bézier curve</a></li>
|
||||
<li><a href="zh-CN/index.html#moulding">Manipulating a curve</a></li>
|
||||
<li><a href="zh-CN/index.html#pointcurves">Creating a curve from three points</a></li>
|
||||
<li><a href="zh-CN/index.html#curvefitting">Curve fitting</a></li>
|
||||
@@ -102,7 +103,6 @@
|
||||
<li><a href="zh-CN/index.html#catmullmoulding">Creating a Catmull-Rom curve from three points</a></li>
|
||||
<li><a href="zh-CN/index.html#polybezier">Forming poly-Bézier curves</a></li>
|
||||
<li><a href="zh-CN/index.html#shapes">Boolean shape operations</a></li>
|
||||
<li><a href="zh-CN/index.html#projections">Projecting a point onto a Bézier curve</a></li>
|
||||
<li><a href="zh-CN/index.html#offsetting">Curve offsetting</a></li>
|
||||
<li><a href="zh-CN/index.html#graduatedoffset">Graduated curve offsetting</a></li>
|
||||
<li><a href="zh-CN/index.html#circles">Circles and quadratic Bézier curves</a></li>
|
||||
@@ -217,7 +217,7 @@
|
||||
:
|
||||
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" height="275" src="./chapters/whatis/interpolation.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\d3d0579ce3534dcee1fda11bdf609c22.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\9b3633889c38325c24c19ce18ab94ad6.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
|
||||
@@ -324,7 +324,7 @@ function Bezier(3,t):
|
||||
<div class="figure">
|
||||
<graphics-element title="二次插值" width="275" height="275" src="./chapters/control/lerp.js" data-degree="3">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\62d7ef11f60acde82868424364a477e8.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\ecc15848fbe7b2176b0c89973f07c694.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -332,7 +332,7 @@ function Bezier(3,t):
|
||||
|
||||
<graphics-element title="三次插值" width="275" height="275" src="./chapters/control/lerp.js" data-degree="4">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\9331dd83c72b233190eaca1cfcc169db.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\49423783987ac4bc49fbe4c519dbc1d1.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -340,7 +340,7 @@ function Bezier(3,t):
|
||||
|
||||
<graphics-element title="15次插值" width="275" height="275" src="./chapters/control/lerp.js" data-degree="15">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\control\44242f6a6be718bea46292369d509520.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\control\afd21a9ba16965c2e7ec2d0d14892250.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -595,7 +595,7 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>使用 de Casteljau 算法我们也可以将一条贝塞尔曲线分割成两条更小的曲线,二者拼接起来即可形成原来的曲线。当采用某个 <code>t</code> 值构造 de Casteljau 算法时,该过程会给到我们在 <code>t</code> 点分割曲线的所有点: 一条曲线包含该曲线上点之前的所有点,另一条曲线包含该曲线上点之后的所有点。</p>
|
||||
<graphics-element title="分割一条曲线" width="825" height="275" src="./chapters/splitting/splitting.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\43f3024045439e3bcc95434fa39f9d03.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\bef2f09698c0d3d2b7c4c031be17ff69.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
@@ -827,7 +827,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/frenet.js" >
|
||||
<fallback-image>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\ca775688322e0da5c5ecc5a15e2e57de.png" loading="lazy">
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\cbadec403b99dec015ab084ee10e1671.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -901,7 +901,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/rotation-minimizing.js" >
|
||||
<fallback-image>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\f989c02a1c37d8837e1e50e993415a73.png" loading="lazy">
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\9983328e50c2156c124bb1ea4ad5c05b.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
@@ -919,13 +919,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.</p>
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\components\f7490a1c523d4dc8772b621b4a61fdd4.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\components\008604bc4c53bd7e0d97c99a67812ad1.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\components\35d69b33228ae64221385047177b67a5.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\components\5214256129e6396e7ac1f1713fa9c88d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1061,14 +1061,14 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:</p>
|
||||
<graphics-element title="Quadratic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\6d246f1c53e40bd5156ef50c4046db51.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\9850eec01924deae7fda4400ce44270d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||||
<graphics-element title="Cubic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\05ff4df8b73ba25dffeb42f768e0e9c4.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\890406c7bc96904224f8f14940bf3e56.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1114,12 +1114,12 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\2bac71234aed2bca2686fdbce7dd78d8.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\b3ccd45a72c815388aee6515fe37a486.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\ed3e2ad3961fbf9e9e5bc951f1d79302.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\31655f24b7dd8b8871687b6610d9ac0e.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
@@ -1189,7 +1189,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
<graphics-element title="The canonical curve map" width="400" height="400" src="./chapters/canonical/canonical.js" >
|
||||
<fallback-image>
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\0611789dcef56f4dc544ac806eada228.png" loading="lazy">
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\675217e6df3d75c86503dc2af623e14e.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1255,7 +1255,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:</p>
|
||||
<graphics-element title="A cubic curve mapped to canonical form" width="800" height="400" src="./chapters/canonical/interactive.js" >
|
||||
<fallback-image>
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\3efa5cf9f4bef7b5a656a118a17e5c7b.png" loading="lazy">
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\344346f09b6d5d7e3ddf91084ad50d46.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1266,7 +1266,7 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\a6f705eb306c43e5709970b2ccad9d20.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\dd28d64458d22f4fe89c98568258efcb.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -1293,7 +1293,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So the procedure is fairly straight forward: pick an <code>x</code>, find the associted <code>t</code> value, evaluate our curve <em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this <code>x</code>. Move the slider for the following graphic to see this in action:</p>
|
||||
<graphics-element title="Finding By(t), by finding t for a given x" width="275" height="275" src="./chapters/yforx/yforx.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\e1549cca3a5203c4e4d7fa22948cb3f8.png" loading="lazy">
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\efcfe9b48ca4e65eef3d4bf3e4c97bc3.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
@@ -1440,7 +1440,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\4f2cd306ec6fa0340ac7f410744b3118.png" loading="lazy">
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\52f815cefe99dabc47ca83d0b97b61fc.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
@@ -1448,7 +1448,7 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\25e9697557129c651e9c7cc4e4878b16.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\133bf9d02801a3149c9ddb8b313e6797.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
@@ -1531,7 +1531,7 @@ lli = function(line1, line2):
|
||||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||||
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\a71619a14589851390cf88aa07042d3e.png" loading="lazy">
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\02d70e27ba678db49a883afcc6264c9a.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
@@ -1591,22 +1591,52 @@ lli = function(line1, line2):
|
||||
<img class="LaTeX SVG" src="./images/chapters/abc/12aaf0d7fd20b3c551a0ec76b18bd7d2.svg" width="231px" height="39px" loading="lazy">
|
||||
<p>So: if we have a curve's start and end point, then for any <code>t</code> value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.</p>
|
||||
|
||||
</section>
|
||||
<section id="projections">
|
||||
<h1><a href="zh-CN/index.html#projections">Projecting a point onto a Bézier curve</a></h1>
|
||||
<p>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?</p>
|
||||
<p>If the Bézier curve is of low enough order, we might be able to <a href="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how to do this</a>, and get a perfect <code>t</code> 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 <code>t</code> 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 <code>t</code> values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:</p>
|
||||
<pre><code>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</code></pre>
|
||||
<p>After this runs, we know that <code>LUT[i]</code> is the coordinate on the curve <em>in our LUT</em> 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 <em>somewhere else</em> between those two values, so that's what we're going to be testing for, using a variation of the binary search.</p>
|
||||
<ol>
|
||||
<li>we start with our point <code>p</code>, and the <code>t</code> values <code>t1=LUT[i-1].t</code> and <code>t2=LUT[i+1].t</code>, which span an interval <code>v = t2-t1</code>.</li>
|
||||
<li>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</li>
|
||||
<li>we then check which of these five points is the closest to our original point <code>p</code>, and then repeat step 1 with the points before and after the closest point we just found.</li>
|
||||
</ol>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./chapters/projections/project.js" >
|
||||
<fallback-image>
|
||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
<section id="moulding">
|
||||
<h1><a href="zh-CN/index.html#moulding">Manipulating a curve</a></h1>
|
||||
<p>Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the <em>t</em>-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.</p>
|
||||
<Graphic title="Moulding a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markQB} onMouseDrag={this.dragQB} onMouseUp={this.saveCurve}/>
|
||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/mould-quadratic.js" >
|
||||
<fallback-image>
|
||||
<img width="825px" height="275px" src="images\chapters\moulding\7f080cfc5764282db126164d6705d83d.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p><strong>Click-dragging the curve itself</strong> shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding <i>t</i>-value, the original point C for that <i>t</i>-value, as well as the new point B' based on the mouse cursor. Since we know the <i>t</i>-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="221px" height="36px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="239px" height="35px" loading="lazy">
|
||||
<p>For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point. For cubic curves, we need to do a little more work:</p>
|
||||
<Graphic title="Moulding a cubic Bézier curve" setup={this.setupCubic} draw={this.drawMould} onClick={this.placeMouldPoint} onMouseDown={this.markCB} onMouseDrag={this.dragCB} onMouseUp={this.saveCurve}/>
|
||||
|
||||
<p>To help understand what's going on, the cubic graphic shows the full de Casteljau construction "hull" when repositioning point B. We compute A' in exactly the same way as before, but we also record the final strut line that forms B in the original curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points should be. Remember that B' lies on line e1--e2 at a distance <i>t</i>, because that's how Bézier curves work. In the same manner, we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing the new control points is fairly easy.</p>
|
||||
<p>First, we construct the one-level-of-de-Casteljau-up points:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/524206c49f317d27d8e07a310b24a7a3.svg" width="132px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/524206c49f317d27d8e07a310b24a7a3.svg" width="139px" height="75px" loading="lazy">
|
||||
<p>And then we can compute the new control points:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg" width="156px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg" width="168px" height="72px" loading="lazy">
|
||||
<p>And that's cubic curve manipulation.</p>
|
||||
|
||||
</section>
|
||||
@@ -1876,21 +1906,6 @@ lli = function(line1, line2):
|
||||
<button onclick="() => this.setMode(mode)">mode</button>
|
||||
</Graphic>
|
||||
|
||||
</section>
|
||||
<section id="projections">
|
||||
<h1><a href="zh-CN/index.html#projections">Projecting a point onto a Bézier curve</a></h1>
|
||||
<p>Say we have a Bézier curve and some point, not on the curve, of which we want to know which <code>t</code> 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="https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how to do this</a>, and get a perfect <code>t</code> 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 <code>t</code> 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 <code>t</code> 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 <code>t</code> value we found, start with some small interval around <code>t</code> (1/length_of_LUT on either side is a reasonable start),</li>
|
||||
<li>if the distance to <code>t ± interval/2</code> is larger than the distance to <code>t</code>, try again with the interval reduced to half its original length.</li>
|
||||
<li>if the distance to <code>t ± interval/2</code> is smaller than the distance to <code>t</code>, replace <code>t</code> with the smaller-distance value.</li>
|
||||
<li>after reducing the interval, or changing <code>t</code>, 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 <code>t</code> 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" <code>t</code> value.</p>
|
||||
<Graphic title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
|
||||
</section>
|
||||
<section id="offsetting">
|
||||
<h1><a href="zh-CN/index.html#offsetting">Curve offsetting</a></h1>
|
||||
|