1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-30 19:50:01 +02:00

finished molding

This commit is contained in:
Pomax
2020-09-01 15:54:50 -07:00
parent fbec463127
commit fc13f64451
84 changed files with 789 additions and 795 deletions

View File

@@ -45,10 +45,10 @@ draw() {
line(p1.x, p1.y, p2.x, p2.y);
}
this.drawABCdata(t, A, B, C);
this.drawABCdata(t, A, B, C, hull);
}
drawABCdata(t, A, B, C) {
drawABCdata(t, A, B, C, hull) {
// show the lines between the A/B/C values
setStroke(`#00FF00`);
line(A.x, A.y, B.x, B.y);
@@ -59,10 +59,23 @@ drawABCdata(t, A, B, C) {
// with their associated labels
setFill(`black`);
text(`Using t = ${t.toFixed(2)}`, this.width/2, 10, CENTER);
setTextStroke(`white`, 4);
text(`A`, 10 + A.x, A.y);
text(`B (t = ${t.toFixed(2)})`, 10 + B.x, B.y);
text(`B`, 10 + B.x, B.y);
text(`C`, 10 + C.x, C.y);
if(curve.order === 2) {
text(`e1`, hull[3].x, hull[3].y+3, CENTER);
text(`e2`, hull[4].x, hull[4].y+3, CENTER);
} else {
text(`e1`, hull[7].x, hull[7].y+3, CENTER);
text(`e2`, hull[8].x, hull[8].y+3, CENTER);
text(`v1`, hull[4].x, hull[4].y+3, CENTER);
text(`v2`, hull[6].x, hull[6].y+3, CENTER);
}
// and show the distance ratio, which we see does not change irrespective of whether A/B/C change.
const d1 = dist(A.x, A.y, B.x, B.y);
const d2 = dist(B.x, B.y, C.x, C.y);

View File

@@ -1,6 +1,6 @@
# The projection identity
De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mould" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.
De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point around to change the curve's shape.
How does that work? Succinctly: we run de Casteljau's algorithm in reverse!
@@ -24,6 +24,8 @@ So these graphics show us several things:
1. a point at the tip of the curve construction's "hat": let's call that `A`, as well as
2. our on-curve point give our chosen `t` value: let's call that `B`, and finally,
3. a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that `C`.
4. for both qudratic and cubic curves, two points `e1` and `e2`, which represent the single-to-last step in de Casteljau's algorithm: in the last step, we find `B` at `(1-t) * e1 + t * e2`.
4. for cubic curves, also the points `v1` and `v2`, which together with `A` represent the first step in de Casteljau's algorithm: in the next step, we find `e1` and `e2`.
These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on the curve with some `t` value, the ratio of distances from A to B and B to C is fixed: if some `t` value sets up a C that is 20% away from the start and 80% away from the end, then _it doesn't matter where the start, end, or control points are_; for that `t` value, `C` will *always* lie at 20% from the start and 80% from the end point. Go ahead, pick an on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move, and so neither will C, and if you move either start or end point, C will move but its relative position will not change.
@@ -71,4 +73,22 @@ Which now leaves us with some powerful tools: given thee points (start, end, and
A = B - \frac{C - B}{ratio(t)} = B + \frac{B - C}{ratio(t)}
\]
So: if we have a curve's start and end point, then for any `t` 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.
With `A` found, finding `e1` and `e2` for quadratic curves is a matter of running the linear interpolation with `t` between start and `A` to yield `e1`, and between `A` and end to yield `e2`. For cubic curves, there is no single pair of points that can act as `e1` and `e2`: as long as the distance ratio between `e1` to `B` and `B` to `e2` is the Bézier ratio `(1-t):t`, we can reverse engineer `v1` and `v2`:
\[
\left \{ \begin{aligned}
v_1 &= A' - \frac{A' - e_1}{1 - t} \\
v_2 &= A' - \frac{A' - e_2}{t}
\end{aligned} \right .
\]
And then reverse engineer the curve's control control points:
\[
\left \{ \begin{aligned}
C_1' &= start + \frac{v_1 - start}{t} \\
C_2' &= end + \frac{v_2 - end}{1 - t}
\end{aligned} \right .
\]
So: if we have a curve's start and end point, then for any `t` value we implicitly know all the ABC values, which (combined with an educated guess on appropriate `e1` and `e2` coordinates for cubic curves) 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 "mold" 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 few sections.

View File

@@ -0,0 +1,37 @@
# Molding a curve
Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around.
For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a `t` value and initial `B` coordinate. We don't even need the latter: with our `t` value and "whever the cursor is" as target `B`, we can compute the associated `C`:
\[
C = u(t)_{q} \cdot Start + \left ( 1-u(t)_{q} \right ) \cdot End
\]
And then the associated `A`:
\[
A = B - \frac{C - B}{ratio(t)_{q}} = B + \frac{B - C}{ratio(t)_{q}}
\]
And we're done, because that's our new quadratic control point!
<graphics-element title="Molding a quadratic Bézier curve" width="825" src="./molding.js" data-type="quadratic"></graphics-element>
As before, cubic curves are a bit more work, because while it's easy to find our initial `t` value and ABC values, getting those all-important `e1` and `e2` coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate `t` value ourselves, which allowed us to find appropriate `e1` and `e2` coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own `t` value, and its own `e1` and `e2` values, and those may not make sense for the rest of the curve.
For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its `t` value and `e1`/`e2` coordinates:
<graphics-element title="Molding a cubic Bézier curve" width="825" src="./molding.js" data-type="cubic"></graphics-element>
That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.
One way to combat this might be to combine the above approach with the approach from the [creating curves](#pointcurves) section: generate both the "unchanged `t`/`e1`/`e2`" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised `t` value, and then interpolating between those two curves:
<graphics-element title="Molding a cubic Bézier curve" width="825" src="./molding.js" data-type="cubic" data-interpolated="true">
<input type="range" min="10" max="200" step="1" value="100" class="slide-control">
</graphics-element>
The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it interpolates with a bias towards "preserving `t`/`e1`/`e2`" closer to the original point, and bias towards "idealised" form the further away we move our point, with anything that's further than our falloff distance simply _being_ the idealised curve. We don't even try to interpolate at that point.
A more advanced way to try to smooth things out is to implement _continuous_ molding, where we constantly update the curve as we move around, and constantly change what our `B` point is, based on constantly projecting the cursor on the curve _as we're updating it_ - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation (with a reasonable distance) will do for now!

View File

@@ -0,0 +1,262 @@
let curve, utils = Bezier.getUtils();
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]);
if (this.parameters.interpolated) {
setSlider(`.slide-control`, `falloff`, 100);
}
}
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(5)}`, this.panelWidth/2, 15, CENTER);
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.molded = new Bezier(this, [S,A,E]);
}
}
drawCubicMark() {
const S = curve.points[0],
E = curve.points[curve.order],
{B, t, e1, e2} = this.mark,
org = curve.getABC(t, B),
nB = this.position,
d1 = { x: e1.x - B.x, y: e1.y - B.y },
d2 = { x: e2.x - B.x, y: e2.y - B.y },
ne1 = { x: nB.x + d1.x, y: nB.y + d1.y },
ne2 = { x: nB.x + d2.x, y: nB.y + d2.y },
{A, C} = curve.getABC(t, nB),
{v1, v2, C1, C2} = this.deriveControlPoints(S, A, E, ne1, ne2, t);
if (this.parameters.interpolated) {
const ideal = this.getIdealisedCurve(S, nB, E);
this.ideal = new Bezier(this, [ideal.S, ideal.C1, ideal.C2, ideal.E]);
}
setColor(`black`);
text(`t: ${t}`, this.panelWidth/2, 20, CENTER);
setColor(`lightblue`);
line(S.x,S.y,E.x,E.y);
line(org.C.x,org.C.y,org.A.x,org.A.y);
circle(org.A.x, org.A.y, 3);
circle(org.B.x, org.B.y, 3);
circle(org.C.x, org.C.y, 3);
text(`A`, org.A.x + 5, org.A.y);
text(`B`, org.B.x + 5, org.B.y);
text(`C`, org.C.x + 5, org.C.y);
setColor(`purple`);
circle(A.x, A.y, 3);
circle(nB.x, nB.y, 3);
circle(C.x, C.y, 3);
circle(ne1.x, ne1.y, 2);
circle(ne2.x, ne2.y, 2);
line(v1.x, v1.y, A.x, A.y);
line(v2.x, v2.y, A.x, A.y);
line(S.x,S.y,C1.x,C1.y);
line(E.x,E.y,C2.x,C2.y);
line(C2.x,C2.y,C1.x,C1.y);
line(A.x,A.y,C.x,C.y);
line(ne1.x, ne1.y, ne2.x, ne2.y);
noFill();
circle(nB.x, nB.y, 5);
this.molded = new Bezier(this, [S,C1,C2,E]);
}
deriveControlPoints(S, A, E, e1, e2, t) {
// And then use those to derive the correct v1/v2/C1/C2 coordinates
const v1 = {
x: A.x - (A.x - e1.x)/(1-t),
y: A.y - (A.y - e1.y)/(1-t)
};
const v2 = {
x: A.x - (A.x - e2.x)/t,
y: A.y - (A.y - e2.y)/t
};
const C1 = {
x: S.x + (v1.x - S.x) / t,
y: S.y + (v1.y - S.y) / t
};
const C2 = {
x: E.x + (v2.x - E.x) / (1-t),
y: E.y + (v2.y - E.y) / (1-t)
};
return {v1, v2, C1, C2};
}
getIdealisedCurve(p1, p2, p3) {
const c = utils.getccenter(p1, p2, p3),
d1 = dist(p1.x, p1.y, p2.x, p2.y),
d2 = dist(p3.x, p3.y, p2.x, p2.y),
t = d1 / (d1 + d2),
{ A, B, C, S, E } = Bezier.getABC(3, p1, p2, p3, t),
angle = ( atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x) + TAU ) % TAU,
bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3,
de1 = t * bc,
de2 = (1-t) * bc,
tangent = [
{ x: B.x - 10 * (B.y-c.y), y: B.y + 10 * (B.x-c.x) },
{ x: B.x + 10 * (B.y-c.y), y: B.y - 10 * (B.x-c.x) }
],
tlength = dist(tangent[0].x, tangent[0].y, tangent[1].x, tangent[1].y),
dx = (tangent[1].x - tangent[0].x)/tlength,
dy = (tangent[1].y - tangent[0].y)/tlength,
e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy},
e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy },
{v1, v2, C1, C2} = this.deriveControlPoints(S, A, E, e1, e2, t);
return {A,B,C,S,E,e1,e2,v1,v2,C1,C2};
}
drawResult() {
let last = curve;
if (this.molded) last = this.molded;
last.drawSkeleton(`lightblue`);
last.drawCurve(this.parameters.interpolated ? `lightblue` : `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);
if (this.ideal) {
let d = dist(this.mark.B.x, this.mark.B.y, this.position.x, this.position.y);
let t = min(this.falloff, d) / this.falloff;
this.ideal.drawCurve(`lightblue`);
let iC1 = {
x: (1-t) * last.points[1].x + t * this.ideal.points[1].x,
y: (1-t) * last.points[1].y + t * this.ideal.points[1].y
};
let iC2 = {
x: (1-t) * last.points[2].x + t * this.ideal.points[2].x,
y: (1-t) * last.points[2].y + t * this.ideal.points[2].y
};
this.interpolated = new Bezier(this, [last.points[0], iC1, iC2, last.points[3]]);
this.interpolated.drawCurve();
}
}
}
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);
let m = this.mark = {
t, B: this.position.projection,
e1: struts[7],
e2: struts[8]
};
m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y};
m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y};
}
}
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.molded) {
curve = this.interpolated ?? this.molded;
this.interpolated = false;
this.molded = false;
resetMovable(curve.points, [this.position]);
}
redraw();
}

View File

@@ -1,83 +0,0 @@
# Manipulating a curve
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="./moulding.js" data-type="quadratic"></graphics-element>
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(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, 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:
<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>
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}
v_1 &= A' - \frac{A' - e_1}{1 - t} \\
v_2 &= A' - \frac{A' - e_2}{t}
\end{aligned} \right .
\]
After which computing the new control points is straight-forward:
\[
\left \{ \begin{aligned}
C_1' &= start + \frac{v_1 - start}{t} \\
C_2' &= end + \frac{v_2 - end}{1 - t}
\end{aligned} \right .
\]
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, and want to rearrange those to lead to "cleaner looking" curve manipulation. Unfortunately, there are so many differen ways in which we can do this that figuring out "good looking" alternatives, given what the curve is being manipulated for, could be an entire book on its own... so we're only going to look at one way that you might effect alternative `e1` and `e2` points, based on the idea of rotating a vector.
If we treat point `B` as a "a vector originating at `C`" then we can treat the points `e1` and `e2` as offets (let's call these `d1` and `d2`) of that vector, where:
\[
\left \{ \begin{aligned}
e_1 &= B + d_1 \\
e_2 &= B + d_2
\end{aligned} \right .
\]
Which means that:
\[
\left \{ \begin{aligned}
d_1 &= e_1 - B\\
d_2 &= e_2 - B
\end{aligned} \right .
\]
Now, if we now `B` to some new coordinate `B'` we can treat that "moving of the coordinate" as a rotation and scaling of the vector for `B` instead. If the new point `B'` is the same distance away from `C` as `B` was, this is a pure rotation, but otherwise the length of the vector has decreased or increased by some factor.
We can use both those values to change where `e1` and `e2` end up, and thus how our curve moulding "feels", by placing new `e1'` and `e2'` where:
\[
\left \{ \begin{aligned}
angle &= atan2(B_y-C_y,B_x-C_x) - atan2(B_y\prime-C.y, B_x\prime-C.x) \\
e_1' &= B' + scale \cdot rotate(d_1, B', angle) \\
e_2' &= B' + scale \cdot rotate(d_2, B', angle)
\end{aligned} \right .
\]
Here, the `rotate()` function rotates a vector (in this case `d1` or `d2`) around some point (in this case, `B'`), by some angle (in this case, the angle by which we rotated our original `B` to become `B'`). So what does _that_ look like?
<graphics-element title="Moulding a cubic Bézier curve" width="825" src="./moulding.js" data-type="cubic" data-alternative="true"></graphics-element>
As you can see, this is both better, and worse, depending on what you're trying to do with the curve, and there are many different ways in which you can try to change `e1` and `e2` such that they behave "as users would expect them to" based on the context in which you're implementing curve moulding. You might want to add reflections when `B'` crosses the baseline, or even some kind of weight-swapping when `B'` crosses the midline (perpendicular to the baseline, at its mid point), and instead of scaling both points with respects to `C`, you might want to scale them to coordinates 1/2rd and 2/3rd along the baseline, etc. etc.
There are too many options to go over here, so: the best behaviour is, of course, the behaviour _you_ think is best, and it might be a lot of work to find that and/or implement that!

View File

@@ -1,217 +0,0 @@
let curve, utils = Bezier.getUtils();
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, d1, d2} = this.mark;
let oB = B;
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(B.x, B.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 st1 = { x: B.x + d1.x, y: B.y + d1.y };
let st2 = { x: B.x + d2.x, y: B.y + d2.y };
if (this.parameters.alternative) {
let nlen = dist(B.x, B.y, C.x, C.y);
let scale = nlen/olen;
let angle = atan2(B.y-C.y, B.x-C.x) - atan2(oB.y-C.y, oB.x-C.x);
st1 = {
x: B.x + scale * d1.x * cos(angle) - scale * d1.y * sin(angle),
y: B.y + scale * d1.x * sin(angle) + scale * d1.y * cos(angle)
};
st2 = {
x: B.x + scale * d2.x * cos(angle) - scale * d2.y * sin(angle),
y: B.y + scale * d2.x * sin(angle) + scale * d2.y * cos(angle)
};
}
e1 = st1;
e2 = st2;
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);
let m = this.mark = {
t, B: this.position.projection,
e1: struts[7],
e2: struts[8]
};
m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y};
m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y};
}
}
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();
}

View File

@@ -131,7 +131,7 @@ showCurve(p1, p2, p3, c) {
// Check which length we need to use for our e1-e2 segment,
// corrected for whether B is "above" or "below" the baseline:
const angle = atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x),
const angle = ( atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x) + TAU ) % TAU,
bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3,
de1 = t * bc,
de2 = (1-t) * bc;

View File

@@ -1,8 +1,8 @@
# Creating a curve from three points
Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although
Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having the computer do the rest, to which the answer is: that's exactly what we can now do!
For quadratic curves, things are pretty easy: technically we need a `t` value in order to compute the ratio function used in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and `B` point, and `B` and end point as a ratio, using
For quadratic curves, things are pretty easy. Technically, we'll need a `t` value in order to compute the ratio function used in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and `B` point, and `B` and end point as a ratio, using
\[
\left \{ \begin{aligned}
@@ -34,7 +34,7 @@ With that covered, we now also know the tangent line to our point `B`, because t
Where `d` is the total length of the line segment from `e1` to `e2`. So how long do we make that? There are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the length of the baseline". This forces `e1` and `e2` to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually _look_ like a line. Nice! The last thing we'll need to do is make sure to flip the sign of `d` depending on which side of the baseline our `B` is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the [atan2](https://en.wikipedia.org/wiki/Atan2) function:
\[
\phi = atan2(E_y-S_y, E_x-S_x) - atan2(B_y-S_y, B_x-S_x)
\phi = \left ( atan2(E_y-S_y, E_x-S_x) - atan2(B_y-S_y, B_x-S_x) + 2 \pi \right ) \textit{ mod } 2 \pi
\]
This angle φ will be between 0 and π if `B` is "above" the baseline (rotating all three points so that the start is on the left and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value `d`:
@@ -46,7 +46,6 @@ This angle φ will be between 0 and π if `B` is "above" the baseline (rotating
\end{aligned} \right .
\]
The result of this approach looks as follows:
<graphics-element title="Finding the cubic e₁ and e₂ given three points " src="./circle.js" data-show-curve="true"></graphics-element>
@@ -56,3 +55,5 @@ It is important to remember that even though we're using a circular arc to come
<graphics-element title="Fitting a quadratic Bézier curve" src="./cubic.js"></graphics-element>
That looks perfectly servicable!
Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold" curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at implementing curve molding in the section after that, so read on!

View File

@@ -1,6 +1,6 @@
# Projecting a point onto a Bézier curve
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?
Before we can move on to actual curve molding, 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:

View File

@@ -45,14 +45,14 @@ export default [
// curve manipulation
'abc',
'projections',
'moulding',
'pointcurves',
'projections',
'molding',
'curvefitting',
// A quick foray into Catmull-Rom splines
'catmullconv',
'catmullmoulding',
'catmullmolding',
// "things made of more than on curve"
'polybezier',