moulding
@@ -48,8 +48,7 @@ draw() {
|
|||||||
|
|
||||||
// panel 2: the current iteration step
|
// panel 2: the current iteration step
|
||||||
nextPanel();
|
nextPanel();
|
||||||
setStroke(`black`);
|
|
||||||
line(0,0,0,this.height);
|
|
||||||
this.drawIteration();
|
this.drawIteration();
|
||||||
setFill(`black`);
|
setFill(`black`);
|
||||||
let information = `Initial curves, threshold = ${this.epsilon}px`
|
let information = `Initial curves, threshold = ${this.epsilon}px`
|
||||||
@@ -64,8 +63,6 @@ draw() {
|
|||||||
|
|
||||||
// panel 3: intersections
|
// panel 3: intersections
|
||||||
nextPanel();
|
nextPanel();
|
||||||
setStroke(`black`);
|
|
||||||
line(0,0,0,this.height);
|
|
||||||
this.drawIntersections();
|
this.drawIntersections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,36 +2,47 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" src="./mould-quadratic.js"></graphics-element>
|
<graphics-element title="Moulding a quadratic Bézier curve" width="825" src="./moulding.js" data-type="quadratic"></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:
|
Click-dragging a point on the curve 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, and the original points `A` and `C` for that <i>t</i>-value, in light coloring, as well as the new `A'`, `B'`, and `C'` (although of course the `C` coordinates are the same ones, because that's the defining feature of point `C`) based on where you're dragging point `B` to, in purple.
|
||||||
|
|
||||||
|
Since we know the new point `B'`, and the "new" point `C'` as well as the `t` value, we know our new point A' has to be:
|
||||||
|
|
||||||
\[
|
\[
|
||||||
A' = B' - \frac{C - B'}{ratio} = B' + \frac{B' - C}{ratio}
|
A' = B' - \frac{C - B'}{ratio(t)} = B' + \frac{B' - C}{ratio(t)}
|
||||||
\]
|
\]
|
||||||
|
|
||||||
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:
|
For quadratic curves, this means we're done, since the new point `A'` is equivalent to the new quadratic control point.
|
||||||
|
|
||||||
<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}/>
|
For cubic curves, we need to do a little more work, because while computing a new `A'` is exactly the same as before, we're not quite done once we've done so. For cubic curves, `B` has not just an associated `t` value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:
|
||||||
|
|
||||||
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.
|
<graphics-element title="The information necessary to manipulate cubic curves" src="./decasteljau.js">
|
||||||
|
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||||
|
</graphics-element>
|
||||||
|
|
||||||
First, we construct the one-level-of-de-Casteljau-up points:
|
In addition to the `A`, `B`, and `C` values, we also see the points `e1` and `e2`, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points `v1` and `v2`, which we can construct when we know our ABC values enriched with `e1` and `e2`:
|
||||||
|
|
||||||
\[
|
\[
|
||||||
\left \{ \begin{aligned}
|
\left \{ \begin{aligned}
|
||||||
v1 &= e1 - \frac{e1 - A'}{t} \\
|
v1 &= A' - \frac{A' - e1}{1 - t} \\
|
||||||
v2 &= e2 + \frac{e2 - A'}{1 - t}
|
v2 &= A' - \frac{A' - e2}{t}
|
||||||
\end{aligned} \right .
|
\end{aligned} \right .
|
||||||
\]
|
\]
|
||||||
|
|
||||||
And then we can compute the new control points:
|
After which computing the new control points is straight-forward:
|
||||||
|
|
||||||
\[
|
\[
|
||||||
\left \{ \begin{aligned}
|
\left \{ \begin{aligned}
|
||||||
C1' &= v1 + \frac{v1 - start}{t} \\
|
C1' &= start + \frac{v1 - start}{t} \\
|
||||||
C2' &= v2 + \frac{v2 - end}{1 - t}
|
C2' &= end + \frac{v2 - end}{1 - t}
|
||||||
\end{aligned} \right .
|
\end{aligned} \right .
|
||||||
\]
|
\]
|
||||||
|
|
||||||
And that's cubic curve manipulation.
|
So let's put that into practice:
|
||||||
|
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" src="./moulding.js" data-type="cubic"></graphics-element>
|
||||||
|
|
||||||
|
So that looks pretty good, but you may not like having `e1` and `e2` stay the same distances away from `B'` while moving the point around. An alternative is to scale the distances of `e1` and `e2` to `B'` to match the scaling that `A`--`C` undergoes as `A'`--`C`' - whether this looks better or not depends somewhat on your intention as programmer or user, of course, so the following graphic applies this scaling, but it's up to you to decide whether or not that looks better (or, more appropriately, under which circumstances you might want to apply this scaling vs. when you might not):
|
||||||
|
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" src="./moulding.js" data-type="cubic" data-scaling="true"></graphics-element>
|
||||||
|
|
||||||
|
34
docs/chapters/moulding/decasteljau.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
let curve;
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
curve = Bezier.defaultCubic(this);
|
||||||
|
setMovable(curve.points);
|
||||||
|
setSlider(`.slide-control`, `position`, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
clear();
|
||||||
|
const blue = `lightblue`;
|
||||||
|
const t = this.position;
|
||||||
|
const {A,B,C,S,E} = curve.getABC(t);
|
||||||
|
const struts = curve.drawStruts(t, blue, false);
|
||||||
|
curve.drawSkeleton(blue);
|
||||||
|
curve.drawCurve(`black`);
|
||||||
|
setStroke(blue);
|
||||||
|
line(S.x, S.y, E.x, E.y);
|
||||||
|
line(A.x, A.y, C.x, C.y);
|
||||||
|
curve.drawPoints();
|
||||||
|
|
||||||
|
[A,B,C].forEach(p => circle(p.x, p.y, 3));
|
||||||
|
setFill(`black`);
|
||||||
|
text(`A`, A.x+10, A.y);
|
||||||
|
text(`B`, B.x+10, B.y);
|
||||||
|
text(`C`, C.x+10, C.y);
|
||||||
|
setStroke(`purple`);
|
||||||
|
|
||||||
|
const lbl = [`v1`, `v2`, `e1`, `e2`];
|
||||||
|
[struts[4], struts[6], struts[7], struts[8]].forEach((p,i) => {
|
||||||
|
circle(p.x, p.y, 2);
|
||||||
|
text(lbl[i], p.x+10, p.y);
|
||||||
|
});
|
||||||
|
}
|
@@ -1,203 +0,0 @@
|
|||||||
var abs = Math.abs;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setupQuadratic: function(api) {
|
|
||||||
api.setPanelCount(3);
|
|
||||||
var curve = api.getDefaultQuadratic();
|
|
||||||
curve.points[2].x -= 30;
|
|
||||||
api.setCurve(curve);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupCubic: function(api) {
|
|
||||||
api.setPanelCount(3);
|
|
||||||
var curve = new api.Bezier([100,230, 30,160, 200,50, 210,160]);
|
|
||||||
curve.points[2].y -= 20;
|
|
||||||
api.setCurve(curve);
|
|
||||||
api.lut = curve.getLUT(100);
|
|
||||||
},
|
|
||||||
|
|
||||||
saveCurve: function(evt, api) {
|
|
||||||
if (!api.t) return;
|
|
||||||
if (!api.newcurve) return;
|
|
||||||
api.setCurve(api.newcurve);
|
|
||||||
api.t = false;
|
|
||||||
api.redraw();
|
|
||||||
},
|
|
||||||
|
|
||||||
findTValue: function(evt, api) {
|
|
||||||
var t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7);
|
|
||||||
if (t < 0.05 || t > 0.95) return false;
|
|
||||||
return t;
|
|
||||||
},
|
|
||||||
|
|
||||||
markQB: function(evt, api) {
|
|
||||||
api.t = this.findTValue(evt, api);
|
|
||||||
if(api.t) {
|
|
||||||
var t = api.t,
|
|
||||||
t2 = 2*t,
|
|
||||||
top = t2*t - t2,
|
|
||||||
bottom = top + 1,
|
|
||||||
ratio = abs(top/bottom),
|
|
||||||
curve = api.curve,
|
|
||||||
A = api.A = curve.points[1],
|
|
||||||
B = api.B = curve.get(t);
|
|
||||||
api.C = api.utils.lli4(A, B, curve.points[0], curve.points[2]);
|
|
||||||
api.ratio = ratio;
|
|
||||||
this.dragQB(evt, api);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
markCB: function(evt, api) {
|
|
||||||
api.t = this.findTValue(evt, api);
|
|
||||||
if(api.t) {
|
|
||||||
var t = api.t,
|
|
||||||
mt = (1-t),
|
|
||||||
t3 = t*t*t,
|
|
||||||
mt3 = mt*mt*mt,
|
|
||||||
bottom = t3 + mt3,
|
|
||||||
top = bottom - 1,
|
|
||||||
ratio = abs(top/bottom),
|
|
||||||
curve = api.curve,
|
|
||||||
hull = curve.hull(t),
|
|
||||||
A = api.A = hull[5],
|
|
||||||
B = api.B = curve.get(t);
|
|
||||||
api.db = curve.derivative(t);
|
|
||||||
api.C = api.utils.lli4(A, B, curve.points[0], curve.points[3]);
|
|
||||||
api.ratio = ratio;
|
|
||||||
this.dragCB(evt, api);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
drag: function(evt, api) {
|
|
||||||
if (!api.t) return;
|
|
||||||
|
|
||||||
var newB = api.newB = {
|
|
||||||
x: evt.offsetX,
|
|
||||||
y: evt.offsetY
|
|
||||||
};
|
|
||||||
|
|
||||||
// now that we know A, B, C and the AB:BC ratio, we can compute the new A' based on the desired B'
|
|
||||||
api.newA = {
|
|
||||||
x: newB.x - (api.C.x - newB.x) / api.ratio,
|
|
||||||
y: newB.y - (api.C.y - newB.y) / api.ratio
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
dragQB: function(evt, api) {
|
|
||||||
if (!api.t) return;
|
|
||||||
this.drag(evt, api);
|
|
||||||
api.update = [api.newA];
|
|
||||||
},
|
|
||||||
|
|
||||||
dragCB: function(evt, api) {
|
|
||||||
if (!api.t) return;
|
|
||||||
this.drag(evt,api);
|
|
||||||
|
|
||||||
// preserve struts for B when repositioning
|
|
||||||
var curve = api.curve,
|
|
||||||
hull = curve.hull(api.t),
|
|
||||||
B = api.B,
|
|
||||||
Bl = hull[7],
|
|
||||||
Br = hull[8],
|
|
||||||
dbl = { x: Bl.x - B.x, y: Bl.y - B.y },
|
|
||||||
dbr = { x: Br.x - B.x, y: Br.y - B.y },
|
|
||||||
pts = curve.points,
|
|
||||||
// find new point on s--c1
|
|
||||||
p1 = {x: api.newB.x + dbl.x, y: api.newB.y + dbl.y},
|
|
||||||
sc1 = {
|
|
||||||
x: api.newA.x - (api.newA.x - p1.x)/(1-api.t),
|
|
||||||
y: api.newA.y - (api.newA.y - p1.y)/(1-api.t)
|
|
||||||
},
|
|
||||||
// find new point on c2--e
|
|
||||||
p2 = {x: api.newB.x + dbr.x, y: api.newB.y + dbr.y},
|
|
||||||
sc2 = {
|
|
||||||
x: api.newA.x + (p2.x - api.newA.x)/(api.t),
|
|
||||||
y: api.newA.y + (p2.y - api.newA.y)/(api.t)
|
|
||||||
},
|
|
||||||
// construct new c1` based on the fact that s--sc1 is s--c1 * t
|
|
||||||
nc1 = {
|
|
||||||
x: pts[0].x + (sc1.x - pts[0].x)/(api.t),
|
|
||||||
y: pts[0].y + (sc1.y - pts[0].y)/(api.t)
|
|
||||||
},
|
|
||||||
// construct new c2` based on the fact that e--sc2 is e--c2 * (1-t)
|
|
||||||
nc2 = {
|
|
||||||
x: pts[3].x - (pts[3].x - sc2.x)/(1-api.t),
|
|
||||||
y: pts[3].y - (pts[3].y - sc2.y)/(1-api.t)
|
|
||||||
};
|
|
||||||
|
|
||||||
api.p1 = p1;
|
|
||||||
api.p2 = p2;
|
|
||||||
api.sc1 = sc1;
|
|
||||||
api.sc2 = sc2;
|
|
||||||
api.nc1 = nc1;
|
|
||||||
api.nc2 = nc2;
|
|
||||||
|
|
||||||
api.update = [nc1, nc2];
|
|
||||||
},
|
|
||||||
|
|
||||||
drawMould: function(api, curve) {
|
|
||||||
api.reset();
|
|
||||||
|
|
||||||
api.drawSkeleton(curve);
|
|
||||||
api.drawCurve(curve);
|
|
||||||
|
|
||||||
var w = api.getPanelWidth(),
|
|
||||||
h = api.getPanelHeight(),
|
|
||||||
offset = {x:w, y:0},
|
|
||||||
round = api.utils.round;
|
|
||||||
|
|
||||||
api.setColor("black");
|
|
||||||
api.drawLine({x:0,y:0},{x:0,y:h}, offset);
|
|
||||||
api.drawLine({x:w,y:0},{x:w,y:h}, offset);
|
|
||||||
|
|
||||||
if (api.t && api.update) {
|
|
||||||
api.drawCircle(curve.get(api.t),3);
|
|
||||||
api.npts = [curve.points[0]].concat(api.update).concat([curve.points.slice(-1)[0]]);
|
|
||||||
api.newcurve = new api.Bezier(api.npts);
|
|
||||||
|
|
||||||
api.setColor("lightgrey");
|
|
||||||
api.drawCurve(api.newcurve);
|
|
||||||
var newhull = api.drawHull(api.newcurve, api.t, offset);
|
|
||||||
api.drawLine(api.npts[0], api.npts.slice(-1)[0], offset);
|
|
||||||
api.drawLine(api.newA, api.newB, offset);
|
|
||||||
|
|
||||||
api.setColor("grey");
|
|
||||||
api.drawCircle(api.newA, 3, offset);
|
|
||||||
api.setColor("blue");
|
|
||||||
api.drawCircle(api.B, 3, offset);
|
|
||||||
api.drawCircle(api.C, 3, offset);
|
|
||||||
api.drawCircle(api.newB, 3, offset);
|
|
||||||
api.drawLine(api.B, api.C, offset);
|
|
||||||
api.drawLine(api.newB, api.C, offset);
|
|
||||||
|
|
||||||
api.setFill("black");
|
|
||||||
api.text("A'", api.newA, {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
api.text("start", curve.get(0), {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
api.text("end", curve.get(1), {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
api.setFill("blue");
|
|
||||||
api.text("B'", api.newB, {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
api.text("B, at t = "+round(api.t,2), api.B, {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
api.text("C", api.C, {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
|
|
||||||
if(curve.order === 3) {
|
|
||||||
var hull = curve.hull(api.t);
|
|
||||||
api.drawLine(hull[7], hull[8], offset);
|
|
||||||
api.drawLine(newhull[7], newhull[8], offset);
|
|
||||||
api.drawCircle(newhull[7], 3, offset);
|
|
||||||
api.drawCircle(newhull[8], 3, offset);
|
|
||||||
api.text("e1", newhull[7], {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
api.text("e2", newhull[8], {x:offset.x + 7, y:offset.y + 1});
|
|
||||||
}
|
|
||||||
|
|
||||||
offset.x += w;
|
|
||||||
|
|
||||||
api.setColor("lightgrey");
|
|
||||||
api.drawSkeleton(api.newcurve, offset);
|
|
||||||
api.setColor("black");
|
|
||||||
api.drawCurve(api.newcurve, offset);
|
|
||||||
} else {
|
|
||||||
offset.x += w;
|
|
||||||
api.drawCurve(curve, offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,110 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
199
docs/chapters/moulding/moulding.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
let curve;
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
setPanelCount(3);
|
||||||
|
const type = this.type = this.parameters.type ?? `quadratic`;
|
||||||
|
curve = type === `quadratic` ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this);
|
||||||
|
this.position = {x:0,y:0};
|
||||||
|
setMovable(curve.points, [this.position]);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
clear();
|
||||||
|
|
||||||
|
curve.drawSkeleton();
|
||||||
|
curve.drawCurve();
|
||||||
|
curve.drawPoints();
|
||||||
|
this.drawPosition();
|
||||||
|
|
||||||
|
nextPanel();
|
||||||
|
|
||||||
|
curve.drawSkeleton(`lightblue`);
|
||||||
|
curve.drawCurve(`lightblue`);
|
||||||
|
curve.points.forEach(p => circle(p.x, p.y, 2));
|
||||||
|
this.drawMark();
|
||||||
|
|
||||||
|
nextPanel();
|
||||||
|
|
||||||
|
this.drawResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPosition() {
|
||||||
|
if (!this.position) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMark() {
|
||||||
|
if (!this.mark) return;
|
||||||
|
if (this.type === `quadratic`) {
|
||||||
|
this.drawQuadraticMark();
|
||||||
|
} else {
|
||||||
|
this.drawCubicMark();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawQuadraticMark() {
|
||||||
|
let {B, t} = this.mark;
|
||||||
|
setFill(`black`);
|
||||||
|
text(`t = ${t.toFixed(2)}`, B.x + 5, B.y + 10);
|
||||||
|
|
||||||
|
let {A, C, S, E} = curve.getABC(t, B);
|
||||||
|
setColor(`lightblue`);
|
||||||
|
line(S.x, S.y, E.x, E.y);
|
||||||
|
line(A.x, A.y, C.x, C.y);
|
||||||
|
|
||||||
|
const lbl = [`A`, `B`, `C`];
|
||||||
|
[A,B,C].forEach((p,i) => {
|
||||||
|
circle(p.x, p.y, 3);
|
||||||
|
text(lbl[i], p.x + 10, p.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.currentPoint) {
|
||||||
|
let {A,B,C,S,E} = curve.getABC(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);
|
||||||
|
[A,B,C].forEach(p => circle(p.x, p.y, 3));
|
||||||
|
|
||||||
|
noFill();
|
||||||
|
circle(B.x, B.y, 5);
|
||||||
|
this.moulded = new Bezier(this, [S,A,E]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCubicMark() {
|
||||||
|
let {B, t, e1, e2} = this.mark;
|
||||||
|
let d1 = { x: e1.x - B.x, y: e1.y - B.y};
|
||||||
|
let d2 = { x: e2.x - B.x, y: e2.y - B.y};
|
||||||
|
setFill(`black`);
|
||||||
|
text(`t = ${t.toFixed(2)}`, B.x + 5, B.y + 10);
|
||||||
|
|
||||||
|
let {A, C, S, E} = curve.getABC(this.mark.t, B);
|
||||||
|
let olen = dist(A.x, A.y, C.x, C.y);
|
||||||
|
setColor(`lightblue`);
|
||||||
|
line(S.x, S.y, E.x, E.y);
|
||||||
|
line(A.x, A.y, C.x, C.y);
|
||||||
|
|
||||||
|
const lbl = [`A`, `B`, `C`, `e1`, `e2`];
|
||||||
|
[A,B,C,e1,e2].forEach((p,i) => {
|
||||||
|
circle(p.x, p.y, 3);
|
||||||
|
text(lbl[i], p.x + 10, p.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.currentPoint) {
|
||||||
|
let {A,B,C,S,E} = curve.getABC(this.mark.t, this.position);
|
||||||
|
|
||||||
|
let nlen = dist(A.x, A.y, C.x, C.y);
|
||||||
|
let f = this.parameters.scaling ? nlen/olen : 1;
|
||||||
|
let e1 = { x: B.x + f * d1.x, y: B.y + f * d1.y };
|
||||||
|
let e2 = { x: B.x + f * d2.x, y: B.y + f * d2.y };
|
||||||
|
|
||||||
|
setColor(`purple`);
|
||||||
|
line(A.x, A.y, C.x, C.y);
|
||||||
|
line(e1.x, e1.y, e2.x, e2.y);
|
||||||
|
|
||||||
|
let v1 = {
|
||||||
|
x: A.x - (A.x - e1.x)/(1-t),
|
||||||
|
y: A.y - (A.y - e1.y)/(1-t)
|
||||||
|
};
|
||||||
|
|
||||||
|
let v2 = {
|
||||||
|
x: A.x - (A.x - e2.x)/t,
|
||||||
|
y: A.y - (A.y - e2.y)/t
|
||||||
|
};
|
||||||
|
|
||||||
|
let C1 = {
|
||||||
|
x: S.x + (v1.x - S.x) / t,
|
||||||
|
y: S.y + (v1.y - S.y) / t
|
||||||
|
};
|
||||||
|
let C2 = {
|
||||||
|
x: E.x + (v2.x - E.x) / (1-t),
|
||||||
|
y: E.y + (v2.y - E.y) / (1-t)
|
||||||
|
};
|
||||||
|
|
||||||
|
[A,B,C,e1,e2,v1,v2,C1,C2].forEach(p => circle(p.x, p.y, 3));
|
||||||
|
|
||||||
|
noFill();
|
||||||
|
circle(B.x, B.y, 5);
|
||||||
|
this.moulded = new Bezier(this, [S,C1,C2,E]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawResult() {
|
||||||
|
let last = curve;
|
||||||
|
if (this.moulded) last = this.moulded;
|
||||||
|
|
||||||
|
last.drawSkeleton(`lightblue`);
|
||||||
|
last.drawCurve(`black`);
|
||||||
|
last.points.forEach(p => circle(p.x, p.y, 2));
|
||||||
|
|
||||||
|
if (this.mark) {
|
||||||
|
let t = this.mark.t;
|
||||||
|
let B = last.get(t);
|
||||||
|
circle(B.x, B.y, 3);
|
||||||
|
setFill(`black`);
|
||||||
|
text(`t = ${this.mark.t.toFixed(2)}`, B.x + 5, B.y + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown() {
|
||||||
|
if (this.currentPoint !== this.position) {
|
||||||
|
this.mark = false;
|
||||||
|
this.position.projection = false;
|
||||||
|
}
|
||||||
|
else if (this.position.projection) {
|
||||||
|
let t = this.position.projection.t;
|
||||||
|
if (this.type === `quadratic`) {
|
||||||
|
this.mark = {
|
||||||
|
t, B: this.position.projection,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let struts = curve.getStrutPoints(t);
|
||||||
|
this.mark = {
|
||||||
|
t, B: this.position.projection,
|
||||||
|
e1: struts[7],
|
||||||
|
e2: struts[8]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
@@ -22,6 +22,6 @@ After this runs, we know that `LUT[i]` is the coordinate on the curve _in our LU
|
|||||||
|
|
||||||
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.
|
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.
|
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 Bézier 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>
|
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./project.js"></graphics-element>
|
||||||
|
Before Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 14 KiB |
@@ -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>
|
<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" >
|
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="825px" height="275px" src="images\chapters\curveintersection\02d70e27ba678db49a883afcc6264c9a.png" loading="lazy">
|
<img width="825px" height="275px" src="images\chapters\curveintersection\3689ed0c15eace45a1f6ae03909ad8ed.png" loading="lazy">
|
||||||
Scripts are disabled. Showing fallback image.
|
Scripts are disabled. Showing fallback image.
|
||||||
</fallback-image>
|
</fallback-image>
|
||||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||||
@@ -1619,7 +1619,7 @@ for (coordinate, index) in LUT:
|
|||||||
<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>
|
<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>
|
</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>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>
|
<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 Bézier 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" >
|
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./chapters/projections/project.js" >
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
||||||
@@ -1630,23 +1630,43 @@ for (coordinate, index) in LUT:
|
|||||||
<section id="moulding">
|
<section id="moulding">
|
||||||
<h1><a href="#moulding">Manipulating a curve</a></h1>
|
<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>
|
<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>
|
||||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/mould-quadratic.js" >
|
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="quadratic">
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="825px" height="275px" src="images\chapters\moulding\7f080cfc5764282db126164d6705d83d.png" loading="lazy">
|
<img width="825px" height="275px" src="images\chapters\moulding\53cf83a9d9f2173d3212ca87f380f071.png" loading="lazy">
|
||||||
Scripts are disabled. Showing fallback image.
|
Scripts are disabled. Showing fallback image.
|
||||||
</fallback-image></graphics-element>
|
</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>
|
<p>Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point <code>B</code> and its corresponding <i>t</i>-value, and the original points <code>A</code> and <code>C</code> for that <i>t</i>-value, in light coloring, as well as the new <code>A'</code>, <code>B'</code>, and <code>C'</code> (although of course the <code>C</code> coordinates are the same ones, because that's the defining feature of point <code>C</code>) based on where you're dragging point <code>B</code> to, in purple.</p>
|
||||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="239px" height="35px" loading="lazy">
|
<p>Since we know the new point <code>B'</code>, and the "new" point <code>C'</code> as well as the <code>t</code> value, we know our new point A' has to be:</p>
|
||||||
<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>
|
<img class="LaTeX SVG" src="./images/chapters/moulding/7159e1e6eeab38c5aba225fe7553dbe6.svg" width="247px" height="39px" loading="lazy">
|
||||||
<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>For quadratic curves, this means we're done, since the new point <code>A'</code> is equivalent to the new quadratic control point.</p>
|
||||||
|
<p>For cubic curves, we need to do a little more work, because while computing a new <code>A'</code> is exactly the same as before, we're not quite done once we've done so. For cubic curves, <code>B</code> has not just an associated <code>t</code> value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:</p>
|
||||||
|
<graphics-element title="The information necessary to manipulate cubic curves" width="275" height="275" src="./chapters/moulding/decasteljau.js" >
|
||||||
|
<fallback-image>
|
||||||
|
<img width="275px" height="275px" src="images\chapters\moulding\fbc7d78dd768eec4314eb4b5320f2ce5.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">
|
||||||
|
</graphics-element>
|
||||||
|
|
||||||
|
<p>In addition to the <code>A</code>, <code>B</code>, and <code>C</code> values, we also see the points <code>e1</code> and <code>e2</code>, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points <code>v1</code> and <code>v2</code>, which we can construct when we know our ABC values enriched with <code>e1</code> and <code>e2</code>:</p>
|
||||||
|
<img class="LaTeX SVG" src="./images/chapters/moulding/8e99a48e3227c97233c4933b5adcb080.svg" width="140px" height="75px" loading="lazy">
|
||||||
|
<p>After which computing the new control points is straight-forward:</p>
|
||||||
|
<img class="LaTeX SVG" src="./images/chapters/moulding/e787b4456ccf03c0481f1731c5c32add.svg" width="187px" height="72px" loading="lazy">
|
||||||
|
<p>So let's put that into practice:</p>
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="cubic">
|
||||||
|
<fallback-image>
|
||||||
|
<img width="825px" height="275px" src="images\chapters\moulding\a0df70324a97e780df772a837c5f1c31.png" loading="lazy">
|
||||||
|
Scripts are disabled. Showing fallback image.
|
||||||
|
</fallback-image></graphics-element>
|
||||||
|
|
||||||
|
<p>So that looks pretty good, but you may not like having <code>e1</code> and <code>e2</code> stay the same distances away from <code>B'</code> while moving the point around. An alternative is to scale the distances of <code>e1</code> and <code>e2</code> to <code>B'</code> to match the scaling that <code>A</code>--<code>C</code> undergoes as <code>A'</code>--<code>C</code>' - whether this looks better or not depends somewhat on your intention as programmer or user, of course, so the following graphic applies this scaling, but it's up to you to decide whether or not that looks better (or, more appropriately, under which circumstances you might want to apply this scaling vs. when you might not):</p>
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="cubic" data-scaling="true">
|
||||||
|
<fallback-image>
|
||||||
|
<img width="825px" height="275px" src="images\chapters\moulding\9de1da4f061d324415321a5ef688f067.png" loading="lazy">
|
||||||
|
Scripts are disabled. Showing fallback image.
|
||||||
|
</fallback-image></graphics-element>
|
||||||
|
|
||||||
<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="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="168px" height="72px" loading="lazy">
|
|
||||||
<p>And that's cubic curve manipulation.</p>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<section id="pointcurves">
|
<section id="pointcurves">
|
||||||
|
@@ -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>
|
<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" >
|
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="825px" height="275px" src="images\chapters\curveintersection\02d70e27ba678db49a883afcc6264c9a.png" loading="lazy">
|
<img width="825px" height="275px" src="images\chapters\curveintersection\3689ed0c15eace45a1f6ae03909ad8ed.png" loading="lazy">
|
||||||
Scripts are disabled. Showing fallback image.
|
Scripts are disabled. Showing fallback image.
|
||||||
</fallback-image>
|
</fallback-image>
|
||||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||||
@@ -1616,7 +1616,7 @@ for (coordinate, index) in LUT:
|
|||||||
<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>
|
<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>
|
</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>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>
|
<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 Bézier 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" >
|
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./chapters/projections/project.js" >
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
||||||
@@ -1627,23 +1627,43 @@ for (coordinate, index) in LUT:
|
|||||||
<section id="moulding">
|
<section id="moulding">
|
||||||
<h1><a href="ja-JP/index.html#moulding">Manipulating a curve</a></h1>
|
<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>
|
<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>
|
||||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/mould-quadratic.js" >
|
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="quadratic">
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="825px" height="275px" src="images\chapters\moulding\7f080cfc5764282db126164d6705d83d.png" loading="lazy">
|
<img width="825px" height="275px" src="images\chapters\moulding\53cf83a9d9f2173d3212ca87f380f071.png" loading="lazy">
|
||||||
Scripts are disabled. Showing fallback image.
|
Scripts are disabled. Showing fallback image.
|
||||||
</fallback-image></graphics-element>
|
</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>
|
<p>Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point <code>B</code> and its corresponding <i>t</i>-value, and the original points <code>A</code> and <code>C</code> for that <i>t</i>-value, in light coloring, as well as the new <code>A'</code>, <code>B'</code>, and <code>C'</code> (although of course the <code>C</code> coordinates are the same ones, because that's the defining feature of point <code>C</code>) based on where you're dragging point <code>B</code> to, in purple.</p>
|
||||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="239px" height="35px" loading="lazy">
|
<p>Since we know the new point <code>B'</code>, and the "new" point <code>C'</code> as well as the <code>t</code> value, we know our new point A' has to be:</p>
|
||||||
<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>
|
<img class="LaTeX SVG" src="./images/chapters/moulding/7159e1e6eeab38c5aba225fe7553dbe6.svg" width="247px" height="39px" loading="lazy">
|
||||||
<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>For quadratic curves, this means we're done, since the new point <code>A'</code> is equivalent to the new quadratic control point.</p>
|
||||||
|
<p>For cubic curves, we need to do a little more work, because while computing a new <code>A'</code> is exactly the same as before, we're not quite done once we've done so. For cubic curves, <code>B</code> has not just an associated <code>t</code> value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:</p>
|
||||||
|
<graphics-element title="The information necessary to manipulate cubic curves" width="275" height="275" src="./chapters/moulding/decasteljau.js" >
|
||||||
|
<fallback-image>
|
||||||
|
<img width="275px" height="275px" src="images\chapters\moulding\fbc7d78dd768eec4314eb4b5320f2ce5.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">
|
||||||
|
</graphics-element>
|
||||||
|
|
||||||
|
<p>In addition to the <code>A</code>, <code>B</code>, and <code>C</code> values, we also see the points <code>e1</code> and <code>e2</code>, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points <code>v1</code> and <code>v2</code>, which we can construct when we know our ABC values enriched with <code>e1</code> and <code>e2</code>:</p>
|
||||||
|
<img class="LaTeX SVG" src="./images/chapters/moulding/8e99a48e3227c97233c4933b5adcb080.svg" width="140px" height="75px" loading="lazy">
|
||||||
|
<p>After which computing the new control points is straight-forward:</p>
|
||||||
|
<img class="LaTeX SVG" src="./images/chapters/moulding/e787b4456ccf03c0481f1731c5c32add.svg" width="187px" height="72px" loading="lazy">
|
||||||
|
<p>So let's put that into practice:</p>
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="cubic">
|
||||||
|
<fallback-image>
|
||||||
|
<img width="825px" height="275px" src="images\chapters\moulding\a0df70324a97e780df772a837c5f1c31.png" loading="lazy">
|
||||||
|
Scripts are disabled. Showing fallback image.
|
||||||
|
</fallback-image></graphics-element>
|
||||||
|
|
||||||
|
<p>So that looks pretty good, but you may not like having <code>e1</code> and <code>e2</code> stay the same distances away from <code>B'</code> while moving the point around. An alternative is to scale the distances of <code>e1</code> and <code>e2</code> to <code>B'</code> to match the scaling that <code>A</code>--<code>C</code> undergoes as <code>A'</code>--<code>C</code>' - whether this looks better or not depends somewhat on your intention as programmer or user, of course, so the following graphic applies this scaling, but it's up to you to decide whether or not that looks better (or, more appropriately, under which circumstances you might want to apply this scaling vs. when you might not):</p>
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="cubic" data-scaling="true">
|
||||||
|
<fallback-image>
|
||||||
|
<img width="825px" height="275px" src="images\chapters\moulding\9de1da4f061d324415321a5ef688f067.png" loading="lazy">
|
||||||
|
Scripts are disabled. Showing fallback image.
|
||||||
|
</fallback-image></graphics-element>
|
||||||
|
|
||||||
<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="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="168px" height="72px" loading="lazy">
|
|
||||||
<p>And that's cubic curve manipulation.</p>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<section id="pointcurves">
|
<section id="pointcurves">
|
||||||
|
@@ -142,8 +142,10 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
/**
|
/**
|
||||||
* Multi-panel graphics: set up (0,0) to the next panel's start
|
* Multi-panel graphics: set up (0,0) to the next panel's start
|
||||||
*/
|
*/
|
||||||
nextPanel(c) {
|
nextPanel(color = `black`) {
|
||||||
this.translate(this.panelWidth, 0);
|
this.translate(this.panelWidth, 0);
|
||||||
|
this.setStroke(color);
|
||||||
|
this.line(0, 0, 0, this.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -118,7 +118,7 @@ class Bezier extends Original {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStruts(t, color = `black`) {
|
drawStruts(t, color = `black`, showpoints = true) {
|
||||||
const p = t.forEach ? t : this.getStrutPoints(t);
|
const p = t.forEach ? t : this.getStrutPoints(t);
|
||||||
|
|
||||||
const api = this.api;
|
const api = this.api;
|
||||||
@@ -134,7 +134,7 @@ class Bezier extends Original {
|
|||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
let pt = p[s + i];
|
let pt = p[s + i];
|
||||||
api.vertex(pt.x, pt.y);
|
api.vertex(pt.x, pt.y);
|
||||||
api.circle(pt.x, pt.y, 5);
|
if (showpoints) api.circle(pt.x, pt.y, 5);
|
||||||
}
|
}
|
||||||
api.end();
|
api.end();
|
||||||
s += n;
|
s += n;
|
||||||
|
@@ -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>
|
<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" >
|
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="825px" height="275px" src="images\chapters\curveintersection\02d70e27ba678db49a883afcc6264c9a.png" loading="lazy">
|
<img width="825px" height="275px" src="images\chapters\curveintersection\3689ed0c15eace45a1f6ae03909ad8ed.png" loading="lazy">
|
||||||
Scripts are disabled. Showing fallback image.
|
Scripts are disabled. Showing fallback image.
|
||||||
</fallback-image>
|
</fallback-image>
|
||||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||||
@@ -1610,7 +1610,7 @@ for (coordinate, index) in LUT:
|
|||||||
<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>
|
<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>
|
</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>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>
|
<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 Bézier 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" >
|
<graphics-element title="Projecting a point onto a Bézier curve" width="320" height="320" src="./chapters/projections/project.js" >
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png" loading="lazy">
|
||||||
@@ -1621,23 +1621,43 @@ for (coordinate, index) in LUT:
|
|||||||
<section id="moulding">
|
<section id="moulding">
|
||||||
<h1><a href="zh-CN/index.html#moulding">Manipulating a curve</a></h1>
|
<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>
|
<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>
|
||||||
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/mould-quadratic.js" >
|
<graphics-element title="Moulding a quadratic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="quadratic">
|
||||||
<fallback-image>
|
<fallback-image>
|
||||||
<img width="825px" height="275px" src="images\chapters\moulding\7f080cfc5764282db126164d6705d83d.png" loading="lazy">
|
<img width="825px" height="275px" src="images\chapters\moulding\53cf83a9d9f2173d3212ca87f380f071.png" loading="lazy">
|
||||||
Scripts are disabled. Showing fallback image.
|
Scripts are disabled. Showing fallback image.
|
||||||
</fallback-image></graphics-element>
|
</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>
|
<p>Click-dragging a point on the curve shows what we're using to compute the new coordinates: while dragging you will see the original point <code>B</code> and its corresponding <i>t</i>-value, and the original points <code>A</code> and <code>C</code> for that <i>t</i>-value, in light coloring, as well as the new <code>A'</code>, <code>B'</code>, and <code>C'</code> (although of course the <code>C</code> coordinates are the same ones, because that's the defining feature of point <code>C</code>) based on where you're dragging point <code>B</code> to, in purple.</p>
|
||||||
<img class="LaTeX SVG" src="./images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg" width="239px" height="35px" loading="lazy">
|
<p>Since we know the new point <code>B'</code>, and the "new" point <code>C'</code> as well as the <code>t</code> value, we know our new point A' has to be:</p>
|
||||||
<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>
|
<img class="LaTeX SVG" src="./images/chapters/moulding/7159e1e6eeab38c5aba225fe7553dbe6.svg" width="247px" height="39px" loading="lazy">
|
||||||
<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>For quadratic curves, this means we're done, since the new point <code>A'</code> is equivalent to the new quadratic control point.</p>
|
||||||
|
<p>For cubic curves, we need to do a little more work, because while computing a new <code>A'</code> is exactly the same as before, we're not quite done once we've done so. For cubic curves, <code>B</code> has not just an associated <code>t</code> value, but also two associated "side" values. Let's revisit the graphic from the chapter on de Casteljau's algorithm, to see what we mean:</p>
|
||||||
|
<graphics-element title="The information necessary to manipulate cubic curves" width="275" height="275" src="./chapters/moulding/decasteljau.js" >
|
||||||
|
<fallback-image>
|
||||||
|
<img width="275px" height="275px" src="images\chapters\moulding\fbc7d78dd768eec4314eb4b5320f2ce5.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">
|
||||||
|
</graphics-element>
|
||||||
|
|
||||||
|
<p>In addition to the <code>A</code>, <code>B</code>, and <code>C</code> values, we also see the points <code>e1</code> and <code>e2</code>, without which constructing our de Casteljau "strut lines" becomes very difficult indeed; as well as the points <code>v1</code> and <code>v2</code>, which we can construct when we know our ABC values enriched with <code>e1</code> and <code>e2</code>:</p>
|
||||||
|
<img class="LaTeX SVG" src="./images/chapters/moulding/8e99a48e3227c97233c4933b5adcb080.svg" width="140px" height="75px" loading="lazy">
|
||||||
|
<p>After which computing the new control points is straight-forward:</p>
|
||||||
|
<img class="LaTeX SVG" src="./images/chapters/moulding/e787b4456ccf03c0481f1731c5c32add.svg" width="187px" height="72px" loading="lazy">
|
||||||
|
<p>So let's put that into practice:</p>
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="cubic">
|
||||||
|
<fallback-image>
|
||||||
|
<img width="825px" height="275px" src="images\chapters\moulding\a0df70324a97e780df772a837c5f1c31.png" loading="lazy">
|
||||||
|
Scripts are disabled. Showing fallback image.
|
||||||
|
</fallback-image></graphics-element>
|
||||||
|
|
||||||
|
<p>So that looks pretty good, but you may not like having <code>e1</code> and <code>e2</code> stay the same distances away from <code>B'</code> while moving the point around. An alternative is to scale the distances of <code>e1</code> and <code>e2</code> to <code>B'</code> to match the scaling that <code>A</code>--<code>C</code> undergoes as <code>A'</code>--<code>C</code>' - whether this looks better or not depends somewhat on your intention as programmer or user, of course, so the following graphic applies this scaling, but it's up to you to decide whether or not that looks better (or, more appropriately, under which circumstances you might want to apply this scaling vs. when you might not):</p>
|
||||||
|
<graphics-element title="Moulding a cubic Bézier curve" width="825" height="275" src="./chapters/moulding/moulding.js" data-type="cubic" data-scaling="true">
|
||||||
|
<fallback-image>
|
||||||
|
<img width="825px" height="275px" src="images\chapters\moulding\9de1da4f061d324415321a5ef688f067.png" loading="lazy">
|
||||||
|
Scripts are disabled. Showing fallback image.
|
||||||
|
</fallback-image></graphics-element>
|
||||||
|
|
||||||
<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="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="168px" height="72px" loading="lazy">
|
|
||||||
<p>And that's cubic curve manipulation.</p>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<section id="pointcurves">
|
<section id="pointcurves">
|
||||||
|