curve from points
158
docs/chapters/pointcurves/circle.js
Normal file
@@ -0,0 +1,158 @@
|
||||
let points = [], utils = Bezier.getUtils();
|
||||
|
||||
setup() {
|
||||
let w = this.width/4;
|
||||
let h = this.height/3;
|
||||
points.push({x: 0.30 * this.width, y: 0.25 * this.height});
|
||||
points.push({x: 0.80 * this.width, y: 0.50 * this.height});
|
||||
points.push({x: 0.30 * this.width, y: 0.75 * this.height});
|
||||
if (this.parameters.showCurve) {
|
||||
points[1].x = 0.66 * this.width;
|
||||
points[1].y = 0.75 * this.height;
|
||||
}
|
||||
setMovable(points);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
|
||||
if (points.length === 3) {
|
||||
let [p1, p2, p3] = points;
|
||||
let c = this.caclulateCenter(p1, p2, p3);
|
||||
|
||||
if (c) {
|
||||
if (this.parameters.showCurve) {
|
||||
this.showCurve(p1, p2, p3, c);
|
||||
} else {
|
||||
this.showChords(p1, p2, p3, c);
|
||||
}
|
||||
|
||||
setColor(`black`);
|
||||
circle(c.x, c.y, 2);
|
||||
text(`center`, c.x+5, c.y+5);
|
||||
|
||||
setFill(`#FF000030`)
|
||||
setStroke(`red`);
|
||||
circle(c.x, c.y, c.r);
|
||||
noFill();
|
||||
setStroke(`black`)
|
||||
arc(c.x, c.y, c.r, c.s, c.e);
|
||||
}
|
||||
}
|
||||
|
||||
setColor(`black`);
|
||||
let lbl = this.parameters.showCurve ? [`start`, `B`, `end`] : [`p1`, `p2`, `p3`];
|
||||
points.forEach((p,i) => {
|
||||
circle(p.x, p.y, 3);
|
||||
text(lbl[i], p.x+7, p.y+7);
|
||||
});
|
||||
}
|
||||
|
||||
caclulateCenter(p1, p2, p3) {
|
||||
// deltas
|
||||
const dx1 = (p2.x - p1.x),
|
||||
dy1 = (p2.y - p1.y),
|
||||
dx2 = (p3.x - p2.x),
|
||||
dy2 = (p3.y - p2.y);
|
||||
|
||||
// perpendiculars (quarter circle turned)
|
||||
const dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2),
|
||||
dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2),
|
||||
dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2),
|
||||
dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2);
|
||||
|
||||
// chord midpoints
|
||||
const mx1 = (p1.x + p2.x)/2,
|
||||
my1 = (p1.y + p2.y)/2,
|
||||
mx2 = (p2.x + p3.x)/2,
|
||||
my2 = (p2.y + p3.y)/2;
|
||||
|
||||
// midpoint offsets
|
||||
const mx1n = mx1 + dx1p,
|
||||
my1n = my1 + dy1p,
|
||||
mx2n = mx2 + dx2p,
|
||||
my2n = my2 + dy2p;
|
||||
|
||||
// intersection of these lines:
|
||||
const i = utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n);
|
||||
|
||||
if (!i) return false;
|
||||
|
||||
const r = utils.dist(i,p1);
|
||||
|
||||
// determine arc start/mid/end values, and direction (cw/ccw correction)
|
||||
let s = atan2(p1.y - i.y, p1.x - i.x),
|
||||
m = atan2(p2.y - i.y, p2.x - i.x),
|
||||
e = atan2(p3.y - i.y, p3.x - i.x),
|
||||
__;
|
||||
|
||||
if (s<e) {
|
||||
if (s>m || m>e) { s += TAU; }
|
||||
if (s>e) { __=e; e=s; s=__; }
|
||||
} else {
|
||||
if (e<m && m<s) { __=e; e=s; s=__; } else { e += TAU; }
|
||||
}
|
||||
|
||||
// assign and done.
|
||||
i.s = s;
|
||||
i.e = e;
|
||||
i.r = r;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
showChords(p1, p2, p3, c) {
|
||||
setStroke( randomColor() );
|
||||
line(p1.x, p1.y, p2.x, p2.y);
|
||||
let m1 = { x: (p1.x+p2.x)/2, y: (p1.y+p2.y)/2 };
|
||||
line(m1.x, m1.y, c.x + (c.x-m1.x)/2, c.y + (c.y-m1.y)/2);
|
||||
|
||||
setStroke( randomColor() );
|
||||
line(p3.x, p3.y, p2.x, p2.y);
|
||||
let m2 = { x: (p3.x+p2.x)/2, y: (p3.y+p2.y)/2 };
|
||||
line(m2.x, m2.y, c.x + (c.x-m2.x)/2, c.y + (c.y-m2.y)/2);
|
||||
|
||||
setStroke( randomColor() );
|
||||
line(p3.x, p3.y, p1.x, p1.y);
|
||||
let m3 = { x: (p3.x+p1.x)/2, y: (p3.y+p1.y)/2 };
|
||||
line(m3.x, m3.y, c.x + (c.x-m3.x)/2, c.y + (c.y-m3.y)/2);
|
||||
}
|
||||
|
||||
showCurve(p1, p2, p3, c) {
|
||||
const 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);
|
||||
|
||||
setStroke(`lightblue`);
|
||||
line(B.x,B.y,S.x,S.y);
|
||||
line(B.x,B.y,E.x,E.y);
|
||||
text(`t=${t.toFixed(2)}`, B.x+10, B.y+20);
|
||||
|
||||
// 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),
|
||||
bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3,
|
||||
de1 = t * bc,
|
||||
de2 = (1-t) * bc;
|
||||
|
||||
// We then determine the circle-aligned slope as normalized dx/dy
|
||||
const 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;
|
||||
|
||||
line(tangent[0].x, tangent[0].y, tangent[1].x, tangent[1].y);
|
||||
|
||||
// and then construct e1 and e2 as per the chapter text
|
||||
const e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy};
|
||||
circle(e1.x, e1.y, 3);
|
||||
text(`e1`, e1.x+15, e1.y+10);
|
||||
|
||||
const e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy };
|
||||
circle(e2.x, e2.y, 3);
|
||||
text(`e2`, e2.x+15, e2.y+10);
|
||||
}
|
@@ -1,13 +1,58 @@
|
||||
# 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. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a *t* value, so we can figure out where point 'C' needs to be.
|
||||
Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although
|
||||
|
||||
The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped *t* to simply be 0.5, lets us bypass some maths, since a *t* value of 0.5 always puts C in the middle of the start--end line segment)
|
||||
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
|
||||
|
||||
<Graphic title="Fitting a quadratic Bézier curve" setup={this.setup} draw={this.drawQuadratic} onClick={this.onClick} />
|
||||
\[
|
||||
\left \{ \begin{aligned}
|
||||
d_1 &= ||\textit{Start} - B||\\
|
||||
d_2 &= ||\textit{End} - B||\\
|
||||
t &= \frac{d_1}{d_1+d_2}
|
||||
\end{aligned} \right .
|
||||
\]
|
||||
|
||||
For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped *t* to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:
|
||||
With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using `A` as our curve's control point:
|
||||
|
||||
<Graphic title="Fitting a cubic Bézier curve" setup={this.setup} draw={this.drawCubic} onClick={this.onClick} />
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" src="./quadratic.js"></graphics-element>
|
||||
|
||||
In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which *t* value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of *t* and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.
|
||||
For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a [chord](https://en.wikipedia.org/wiki/Chord_%28geometry%29), and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.
|
||||
|
||||
That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:
|
||||
|
||||
<graphics-element title="Finding a circle through three points" src="./circle.js"></graphics-element>
|
||||
|
||||
With that covered, we now also know the tangent line to our point `B`, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points `e1` and `e2` on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable `t` value, and then our `e1` and `e2` coordinates must obey the standard de Casteljau rule for linear interpolation:
|
||||
|
||||
\[
|
||||
\left \{ \begin{aligned}
|
||||
e_1 &= B + t \cdot d\\
|
||||
e_2 &= B - (1-t) \cdot d
|
||||
\end{aligned} \right .
|
||||
\]
|
||||
|
||||
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)
|
||||
\]
|
||||
|
||||
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`:
|
||||
|
||||
\[
|
||||
d = \left \{ \begin{aligned}
|
||||
d & \textit{ if } 0 \leq \phi \leq \pi \\
|
||||
-d & \textit{ if } \phi < 0 \lor \phi > \pi
|
||||
\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>
|
||||
|
||||
It is important to remember that even though we're using a circular arc to come up with decent `e1` and `e2` terms, we're _not_ trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; [more on that later](#arcapproximation)), we're _only_ trying to come up with some reasonable `e1` and `e2` points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:
|
||||
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" src="./cubic.js"></graphics-element>
|
||||
|
||||
That looks perfectly servicable!
|
||||
|
96
docs/chapters/pointcurves/cubic.js
Normal file
@@ -0,0 +1,96 @@
|
||||
let points = [], utils = Bezier.getUtils();
|
||||
|
||||
setup() {
|
||||
points = [
|
||||
{x: 0.5 * this.width, y: 0.25 * this.height},
|
||||
{x: 0.75 * this.width, y: 0.5 * this.height},
|
||||
{x: 0.5 * this.width, y: 0.75 * this.height},
|
||||
];
|
||||
setMovable(points);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
let [S, B, E] = points, t;
|
||||
|
||||
if (points.length === 3) {
|
||||
let d1 = dist(S.x, S.y, B.x, B.y);
|
||||
let d2 = dist(E.x, E.y, B.x, B.y);
|
||||
t = d1 / (d1 + d2);
|
||||
|
||||
let {A, C} = Bezier.getABC(3, ...points, t);
|
||||
let { e1, e2, C1, C2 } = this.findControlPoints(t, A, B, C, S, E);
|
||||
|
||||
circle(e1.x, e1.y, 2);
|
||||
circle(e2.x, e2.y, 2);
|
||||
|
||||
let curve = new Bezier(this, [S, C1, C2, E]);
|
||||
|
||||
curve.drawSkeleton(`lightblue`);
|
||||
curve.drawCurve(`grey`);
|
||||
|
||||
setColor(`lightblue`);
|
||||
circle(A.x, A.y, 2);
|
||||
circle(C.x, C.y, 2);
|
||||
text(`A`, A.x+5, A.y+5);
|
||||
line(S.x,S.y,E.x,E.y);
|
||||
line(C.x,C.y,A.x,A.y);
|
||||
text(`C`, C.x+5, C.y+15);
|
||||
}
|
||||
|
||||
setColor(`black`);
|
||||
let lbl = [`Start`, `B`, `End`];
|
||||
points.forEach((p,i) => {
|
||||
circle(p.x, p.y, 3);
|
||||
text(lbl[i], p.x+5, p.y+5);
|
||||
});
|
||||
|
||||
if (points.length === 3) {
|
||||
text(` with t=${t.toFixed(2)}`, B.x + 10, B.y+20);
|
||||
}
|
||||
}
|
||||
|
||||
findControlPoints(t, A, B, C, S, E) {
|
||||
// get our e1-e2 distances
|
||||
const angle = atan2(E.y-S.y, E.x-S.x) - atan2(B.y-S.y, B.x-S.x),
|
||||
bc = (angle < 0 || angle > PI ? -1 : 1) * dist(S.x, S.y, E.x, E.y)/3,
|
||||
de1 = t * bc,
|
||||
de2 = (1-t) * bc;
|
||||
|
||||
// get the circle-aligned slope as normalized dx/dy
|
||||
const c = utils.getccenter(S,B,E),
|
||||
tangent = [
|
||||
{ x: B.x - (B.y - c.y), y: B.y + (B.x - c.x) },
|
||||
{ x: B.x + (B.y - c.y), y: B.y - (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;
|
||||
|
||||
// then set up an e1 and e2 parallel to the baseline
|
||||
const e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy};
|
||||
const e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy };
|
||||
|
||||
// then use those e1/e2 to derive the new hull coordinates
|
||||
const v1 = {
|
||||
x: A.x + (e1.x - A.x) / (1 - t),
|
||||
y: A.y + (e1.y - A.y) / (1 - t)
|
||||
};
|
||||
|
||||
const v2 = {
|
||||
x: A.x + (e2.x - A.x) / t,
|
||||
y: A.y + (e2.y - A.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 { e1, e2, C1, C2 };
|
||||
}
|
@@ -1,164 +0,0 @@
|
||||
var abs = Math.abs;
|
||||
|
||||
module.exports = {
|
||||
setup: function(api) {
|
||||
api.lpts = [
|
||||
{x:56, y:153},
|
||||
{x:144,y:83},
|
||||
{x:188,y:185}
|
||||
];
|
||||
},
|
||||
|
||||
onClick: function(evt, api) {
|
||||
if (api.lpts.length==3) { api.lpts = []; }
|
||||
api.lpts.push({
|
||||
x: evt.offsetX,
|
||||
y: evt.offsetY
|
||||
});
|
||||
api.redraw();
|
||||
},
|
||||
|
||||
getQRatio: function(t) {
|
||||
var t2 = 2*t,
|
||||
top = t2*t - t2,
|
||||
bottom = top + 1;
|
||||
return abs(top/bottom);
|
||||
},
|
||||
|
||||
getCRatio: function(t) {
|
||||
var mt = (1-t),
|
||||
t3 = t*t*t,
|
||||
mt3 = mt*mt*mt,
|
||||
bottom = t3 + mt3,
|
||||
top = bottom - 1;
|
||||
return abs(top/bottom);
|
||||
},
|
||||
|
||||
drawQuadratic: function(api, curve) {
|
||||
var labels = ["start","t=0.5","end"];
|
||||
|
||||
api.reset();
|
||||
|
||||
api.setColor("lightblue");
|
||||
api.drawGrid(10,10);
|
||||
|
||||
api.setFill("black");
|
||||
api.setColor("black");
|
||||
api.lpts.forEach((p,i) => {
|
||||
api.drawCircle(p,3);
|
||||
api.text(labels[i], p, {x:5, y:2});
|
||||
});
|
||||
|
||||
if(api.lpts.length === 3) {
|
||||
var S = api.lpts[0],
|
||||
E = api.lpts[2],
|
||||
B = api.lpts[1],
|
||||
C = {
|
||||
x: (S.x + E.x)/2,
|
||||
y: (S.y + E.y)/2
|
||||
};
|
||||
api.setColor("blue");
|
||||
api.drawLine(S, E);
|
||||
api.drawLine(B, C);
|
||||
api.drawCircle(C, 3);
|
||||
var ratio = this.getQRatio(0.5),
|
||||
A = {
|
||||
x: B.x + (B.x-C.x)/ratio,
|
||||
y: B.y + (B.y-C.y)/ratio
|
||||
};
|
||||
curve = new api.Bezier([S, A, E]);
|
||||
api.setColor("lightgrey");
|
||||
api.drawLine(A, B);
|
||||
api.drawLine(A, S);
|
||||
api.drawLine(A, E);
|
||||
api.setColor("black");
|
||||
api.drawCircle(A, 1);
|
||||
api.drawCurve(curve);
|
||||
}
|
||||
},
|
||||
|
||||
drawCubic: function(api, curve) {
|
||||
var labels = ["start","t=0.5","end"];
|
||||
|
||||
api.reset();
|
||||
|
||||
api.setFill("black");
|
||||
api.setColor("black");
|
||||
api.lpts.forEach((p,i) => {
|
||||
api.drawCircle(p,3);
|
||||
api.text(labels[i], p, {x:5, y:2});
|
||||
});
|
||||
|
||||
api.setColor("lightblue");
|
||||
api.drawGrid(10,10);
|
||||
|
||||
if(api.lpts.length === 3) {
|
||||
var S = api.lpts[0],
|
||||
E = api.lpts[2],
|
||||
B = api.lpts[1],
|
||||
C = {
|
||||
x: (S.x + E.x)/2,
|
||||
y: (S.y + E.y)/2
|
||||
};
|
||||
|
||||
api.setColor("blue");
|
||||
api.drawLine(S, E);
|
||||
api.drawLine(B, C);
|
||||
api.drawCircle(C, 1);
|
||||
|
||||
var ratio = this.getCRatio(0.5),
|
||||
A = {
|
||||
x: B.x + (B.x-C.x)/ratio,
|
||||
y: B.y + (B.y-C.y)/ratio
|
||||
},
|
||||
selen = api.utils.dist(S,E),
|
||||
bclen_min = selen/8,
|
||||
bclen = api.utils.dist(B,C),
|
||||
aesthetics = 4,
|
||||
be12dist = bclen_min + bclen/aesthetics,
|
||||
bx = be12dist * (E.x-S.x)/selen,
|
||||
by = be12dist * (E.y-S.y)/selen,
|
||||
e1 = {
|
||||
x: B.x - bx,
|
||||
y: B.y - by
|
||||
},
|
||||
e2 = {
|
||||
x: B.x + bx,
|
||||
y: B.y + by
|
||||
},
|
||||
|
||||
v1 = {
|
||||
x: A.x + (e1.x-A.x)*2,
|
||||
y: A.y + (e1.y-A.y)*2
|
||||
},
|
||||
v2 = {
|
||||
x: A.x + (e2.x-A.x)*2,
|
||||
y: A.y + (e2.y-A.y)*2
|
||||
},
|
||||
|
||||
nc1 = {
|
||||
x: S.x + (v1.x-S.x)*2,
|
||||
y: S.y + (v1.y-S.y)*2
|
||||
},
|
||||
nc2 = {
|
||||
x: E.x + (v2.x-E.x)*2,
|
||||
y: E.y + (v2.y-E.y)*2
|
||||
};
|
||||
|
||||
curve = new api.Bezier([S, nc1, nc2, E]);
|
||||
api.drawLine(e1, e2);
|
||||
api.setColor("lightgrey");
|
||||
api.drawLine(A, C);
|
||||
api.drawLine(A, v1);
|
||||
api.drawLine(A, v2);
|
||||
api.drawLine(S, nc1);
|
||||
api.drawLine(E, nc2);
|
||||
api.drawLine(nc1, nc2);
|
||||
api.setColor("black");
|
||||
api.drawCircle(A, 1);
|
||||
api.drawCircle(nc1, 1);
|
||||
api.drawCircle(nc2, 1);
|
||||
api.drawCurve(curve);
|
||||
}
|
||||
}
|
||||
};
|
45
docs/chapters/pointcurves/quadratic.js
Normal file
@@ -0,0 +1,45 @@
|
||||
let points;
|
||||
|
||||
setup() {
|
||||
points = [
|
||||
{x: 0.25 * this.width, y: 0.25 * this.height},
|
||||
{x: 0.5 * this.width, y: 0.5 * this.height},
|
||||
{x: 0.25 * this.width, y: 0.75 * this.height},
|
||||
];
|
||||
setMovable(points);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
let [S, B, E] = points, t;
|
||||
|
||||
if (points.length === 3) {
|
||||
let d1 = dist(S.x, S.y, B.x, B.y);
|
||||
let d2 = dist(E.x, E.y, B.x, B.y);
|
||||
t = d1 / (d1 + d2);
|
||||
|
||||
let {A, C} = Bezier.getABC(2, ...points, t);
|
||||
let curve = new Bezier(this, [S, A, E]);
|
||||
|
||||
curve.drawSkeleton(`lightblue`);
|
||||
curve.drawCurve(`grey`);
|
||||
setColor(`lightblue`);
|
||||
circle(A.x, A.y, 2);
|
||||
circle(C.x, C.y, 2);
|
||||
text(`A`, A.x+5, A.y+5);
|
||||
line(S.x,S.y,E.x,E.y);
|
||||
line(C.x,C.y,A.x,A.y);
|
||||
text(`C`, C.x+5, C.y+15);
|
||||
}
|
||||
|
||||
setColor(`black`);
|
||||
let lbl = [`Start`, `B`, `End`];
|
||||
points.forEach((p,i) => {
|
||||
circle(p.x, p.y, 3);
|
||||
text(lbl[i], p.x+5, p.y+5);
|
||||
});
|
||||
|
||||
if (points.length === 3) {
|
||||
text(` with t=${t.toFixed(2)}`, B.x + 10, B.y+5);
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 6.9 KiB |
@@ -1681,14 +1681,45 @@ for (coordinate, index) in LUT:
|
||||
</section>
|
||||
<section id="pointcurves">
|
||||
<h1><a href="#pointcurves">Creating a curve from three points</a></h1>
|
||||
<p>Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a <em>t</em> value, so we can figure out where point 'C' needs to be.</p>
|
||||
<p>The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped <em>t</em> to simply be 0.5, lets us bypass some maths, since a <em>t</em> value of 0.5 always puts C in the middle of the start--end line segment)</p>
|
||||
<Graphic title="Fitting a quadratic Bézier curve" setup={this.setup} draw={this.drawQuadratic} onClick={this.onClick} />
|
||||
<p>Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although</p>
|
||||
<p>For quadratic curves, things are pretty easy: technically we need a <code>t</code> 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 <code>B</code> point, and <code>B</code> and end point as a ratio, using</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/3c7516c16a5dea95df741f4263cecd1c.svg" width="132px" height="85px" loading="lazy">
|
||||
<p>With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using <code>A</code> as our curve's control point:</p>
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" width="275" height="275" src="./chapters/pointcurves/quadratic.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\067a3df30e32708fc0d13f8eb78c0b05.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped <em>t</em> to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:</p>
|
||||
<Graphic title="Fitting a cubic Bézier curve" setup={this.setup} draw={this.drawCubic} onClick={this.onClick} />
|
||||
<p>For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a <a href="https://en.wikipedia.org/wiki/Chord_%28geometry%29">chord</a>, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.</p>
|
||||
<p>That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:</p>
|
||||
<graphics-element title="Finding a circle through three points" width="275" height="275" src="./chapters/pointcurves/circle.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\93ee12566f93f241ad0970acd5505367.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which <em>t</em> value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of <em>t</em> and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.</p>
|
||||
<p>With that covered, we now also know the tangent line to our point <code>B</code>, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points <code>e1</code> and <code>e2</code> on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable <code>t</code> value, and then our <code>e1</code> and <code>e2</code> coordinates must obey the standard de Casteljau rule for linear interpolation:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/55d4f7ed095dfea8f9772208abc83b51.svg" width="147px" height="49px" loading="lazy">
|
||||
<p>Where <code>d</code> is the total length of the line segment from <code>e1</code> to <code>e2</code>. 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 <code>e1</code> and <code>e2</code> to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually <em>look</em> like a line. Nice! The last thing we'll need to do is make sure to flip the sign of <code>d</code> depending on which side of the baseline our <code>B</code> is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the <a href="https://en.wikipedia.org/wiki/Atan2">atan2</a> function:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg" width="393px" height="19px" loading="lazy">
|
||||
<p>This angle φ will be between 0 and π if <code>B</code> 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 <code>d</code>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/6f0e2b6494d7dae2ea79a46a499d7ed4.svg" width="183px" height="49px" loading="lazy">
|
||||
<p>The result of this approach looks as follows:</p>
|
||||
<graphics-element title="Finding the cubic e₁ and e₂ given three points " width="275" height="275" src="./chapters/pointcurves/circle.js" data-show-curve="true">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\8b0e8e79ba09916a9af27b33f8a1919f.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>It is important to remember that even though we're using a circular arc to come up with decent <code>e1</code> and <code>e2</code> terms, we're <em>not</em> trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; <a href="#arcapproximation">more on that later</a>), we're <em>only</em> trying to come up with some reasonable <code>e1</code> and <code>e2</code> points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:</p>
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" width="275" height="275" src="./chapters/pointcurves/cubic.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\eab6ea46fa93030e03ec0ef7deb571dc.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>That looks perfectly servicable!</p>
|
||||
|
||||
</section>
|
||||
<section id="curvefitting">
|
||||
|
@@ -1678,14 +1678,45 @@ for (coordinate, index) in LUT:
|
||||
</section>
|
||||
<section id="pointcurves">
|
||||
<h1><a href="ja-JP/index.html#pointcurves">Creating a curve from three points</a></h1>
|
||||
<p>Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a <em>t</em> value, so we can figure out where point 'C' needs to be.</p>
|
||||
<p>The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped <em>t</em> to simply be 0.5, lets us bypass some maths, since a <em>t</em> value of 0.5 always puts C in the middle of the start--end line segment)</p>
|
||||
<Graphic title="Fitting a quadratic Bézier curve" setup={this.setup} draw={this.drawQuadratic} onClick={this.onClick} />
|
||||
<p>Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although</p>
|
||||
<p>For quadratic curves, things are pretty easy: technically we need a <code>t</code> 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 <code>B</code> point, and <code>B</code> and end point as a ratio, using</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/3c7516c16a5dea95df741f4263cecd1c.svg" width="132px" height="85px" loading="lazy">
|
||||
<p>With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using <code>A</code> as our curve's control point:</p>
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" width="275" height="275" src="./chapters/pointcurves/quadratic.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\067a3df30e32708fc0d13f8eb78c0b05.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped <em>t</em> to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:</p>
|
||||
<Graphic title="Fitting a cubic Bézier curve" setup={this.setup} draw={this.drawCubic} onClick={this.onClick} />
|
||||
<p>For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a <a href="https://en.wikipedia.org/wiki/Chord_%28geometry%29">chord</a>, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.</p>
|
||||
<p>That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:</p>
|
||||
<graphics-element title="Finding a circle through three points" width="275" height="275" src="./chapters/pointcurves/circle.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\93ee12566f93f241ad0970acd5505367.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which <em>t</em> value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of <em>t</em> and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.</p>
|
||||
<p>With that covered, we now also know the tangent line to our point <code>B</code>, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points <code>e1</code> and <code>e2</code> on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable <code>t</code> value, and then our <code>e1</code> and <code>e2</code> coordinates must obey the standard de Casteljau rule for linear interpolation:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/55d4f7ed095dfea8f9772208abc83b51.svg" width="147px" height="49px" loading="lazy">
|
||||
<p>Where <code>d</code> is the total length of the line segment from <code>e1</code> to <code>e2</code>. 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 <code>e1</code> and <code>e2</code> to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually <em>look</em> like a line. Nice! The last thing we'll need to do is make sure to flip the sign of <code>d</code> depending on which side of the baseline our <code>B</code> is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the <a href="https://en.wikipedia.org/wiki/Atan2">atan2</a> function:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg" width="393px" height="19px" loading="lazy">
|
||||
<p>This angle φ will be between 0 and π if <code>B</code> 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 <code>d</code>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/6f0e2b6494d7dae2ea79a46a499d7ed4.svg" width="183px" height="49px" loading="lazy">
|
||||
<p>The result of this approach looks as follows:</p>
|
||||
<graphics-element title="Finding the cubic e₁ and e₂ given three points " width="275" height="275" src="./chapters/pointcurves/circle.js" data-show-curve="true">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\8b0e8e79ba09916a9af27b33f8a1919f.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>It is important to remember that even though we're using a circular arc to come up with decent <code>e1</code> and <code>e2</code> terms, we're <em>not</em> trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; <a href="#arcapproximation">more on that later</a>), we're <em>only</em> trying to come up with some reasonable <code>e1</code> and <code>e2</code> points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:</p>
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" width="275" height="275" src="./chapters/pointcurves/cubic.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\eab6ea46fa93030e03ec0ef7deb571dc.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>That looks perfectly servicable!</p>
|
||||
|
||||
</section>
|
||||
<section id="curvefitting">
|
||||
|
@@ -465,11 +465,18 @@ class GraphicsAPI extends BaseAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a circle around a Point
|
||||
* Draw a circle
|
||||
*/
|
||||
circle(x, y, r) {
|
||||
this.arc(x, y, r, 0, this.TAU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a circular arc
|
||||
*/
|
||||
arc(x, y, r, s, e) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, r, 0, this.TAU);
|
||||
this.ctx.arc(x, y, r, s, e);
|
||||
this.ctx.fill();
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
@@ -238,15 +238,20 @@ class Bezier {
|
||||
return utils.length(this.derivative.bind(this));
|
||||
}
|
||||
|
||||
getABC(t, B) {
|
||||
let S = this.points[0];
|
||||
let E = this.points[this.order];
|
||||
let ret = getABC(this.order, S, B || this.get(t), E, t);
|
||||
static getABC(order = 2, S, B, E, t = 0.5) {
|
||||
let ret = getABC(order, S, B, E, t);
|
||||
ret.S = S;
|
||||
ret.E = E;
|
||||
return ret;
|
||||
}
|
||||
|
||||
getABC(t, B) {
|
||||
B = B || this.get(t);
|
||||
let S = this.points[0];
|
||||
let E = this.points[this.order];
|
||||
return Bezier.getABC(this.order, S, B, E, t);
|
||||
}
|
||||
|
||||
getLUT(steps) {
|
||||
this.verify();
|
||||
steps = steps || 100;
|
||||
|
@@ -1672,14 +1672,45 @@ for (coordinate, index) in LUT:
|
||||
</section>
|
||||
<section id="pointcurves">
|
||||
<h1><a href="zh-CN/index.html#pointcurves">Creating a curve from three points</a></h1>
|
||||
<p>Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points. However, unlike circle-fitting, which requires just three points, Bézier curve fitting requires three points, as well as a <em>t</em> value, so we can figure out where point 'C' needs to be.</p>
|
||||
<p>The following graphic lets you place three points, and will use the preceding sections on the ABC ratio and curve construction to form a quadratic curve through them. You can move the points you've placed around by click-dragging, or try a new curve by drawing new points with pure clicks. (There's some freedom here, so for illustrative purposes we clamped <em>t</em> to simply be 0.5, lets us bypass some maths, since a <em>t</em> value of 0.5 always puts C in the middle of the start--end line segment)</p>
|
||||
<Graphic title="Fitting a quadratic Bézier curve" setup={this.setup} draw={this.drawQuadratic} onClick={this.onClick} />
|
||||
<p>Given the preceding section on curve manipulation, we can also generate quadratic and cubic curves from any three points, although</p>
|
||||
<p>For quadratic curves, things are pretty easy: technically we need a <code>t</code> 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 <code>B</code> point, and <code>B</code> and end point as a ratio, using</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/3c7516c16a5dea95df741f4263cecd1c.svg" width="132px" height="85px" loading="lazy">
|
||||
<p>With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using <code>A</code> as our curve's control point:</p>
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" width="275" height="275" src="./chapters/pointcurves/quadratic.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\067a3df30e32708fc0d13f8eb78c0b05.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>For cubic curves we also need some values to construct the "de Casteljau line through B" with, and that gives us quite a bit of choice. Since we've clamped <em>t</em> to 0.5, we'll set up a line through B parallel to the line start--end, with a length that is proportional to the length of the line B--C: the further away from the baseline B is, the wider its construction line will be, and so the more "bulby" the curve will look. This still gives us some freedom in terms of exactly how to scale the length of the construction line as we move B closer or further away from the baseline, so I simply picked some values that sort-of-kind-of look right in that if a circle through (start,B,end) forms a perfect hemisphere, the cubic curve constructed forms something close to a hemisphere, too, and if the points lie on a line, then the curve constructed has the control points very close to B, while still lying between B and the correct curve end point:</p>
|
||||
<Graphic title="Fitting a cubic Bézier curve" setup={this.setup} draw={this.drawCubic} onClick={this.onClick} />
|
||||
<p>For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a <a href="https://en.wikipedia.org/wiki/Chord_%28geometry%29">chord</a>, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.</p>
|
||||
<p>That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:</p>
|
||||
<graphics-element title="Finding a circle through three points" width="275" height="275" src="./chapters/pointcurves/circle.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\93ee12566f93f241ad0970acd5505367.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>In each graphic, the blue parts are the values that we "just have" simply by setting up our three points, combined with our decision on which <em>t</em> value to use (and construction line orientation and length for cubic curves). There are of course many ways to determine a combination of <em>t</em> and tangent values that lead to a more "æsthetic" curve, but this will be left as an exercise to the reader, since there are many, and æsthetics are often quite personal.</p>
|
||||
<p>With that covered, we now also know the tangent line to our point <code>B</code>, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points <code>e1</code> and <code>e2</code> on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable <code>t</code> value, and then our <code>e1</code> and <code>e2</code> coordinates must obey the standard de Casteljau rule for linear interpolation:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/55d4f7ed095dfea8f9772208abc83b51.svg" width="147px" height="49px" loading="lazy">
|
||||
<p>Where <code>d</code> is the total length of the line segment from <code>e1</code> to <code>e2</code>. 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 <code>e1</code> and <code>e2</code> to always be the "linear curve" distance apart, which means if we place our three points on a line, it will actually <em>look</em> like a line. Nice! The last thing we'll need to do is make sure to flip the sign of <code>d</code> depending on which side of the baseline our <code>B</code> is located, so we don't up creating a funky curve with a loop in it. To do this, we can use the <a href="https://en.wikipedia.org/wiki/Atan2">atan2</a> function:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/b4464f73fb2f79027d8e971fc66813f6.svg" width="393px" height="19px" loading="lazy">
|
||||
<p>This angle φ will be between 0 and π if <code>B</code> 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 <code>d</code>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/pointcurves/6f0e2b6494d7dae2ea79a46a499d7ed4.svg" width="183px" height="49px" loading="lazy">
|
||||
<p>The result of this approach looks as follows:</p>
|
||||
<graphics-element title="Finding the cubic e₁ and e₂ given three points " width="275" height="275" src="./chapters/pointcurves/circle.js" data-show-curve="true">
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\8b0e8e79ba09916a9af27b33f8a1919f.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>It is important to remember that even though we're using a circular arc to come up with decent <code>e1</code> and <code>e2</code> terms, we're <em>not</em> trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; <a href="#arcapproximation">more on that later</a>), we're <em>only</em> trying to come up with some reasonable <code>e1</code> and <code>e2</code> points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:</p>
|
||||
<graphics-element title="Fitting a quadratic Bézier curve" width="275" height="275" src="./chapters/pointcurves/cubic.js" >
|
||||
<fallback-image>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\eab6ea46fa93030e03ec0ef7deb571dc.png" loading="lazy">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>That looks perfectly servicable!</p>
|
||||
|
||||
</section>
|
||||
<section id="curvefitting">
|
||||
|