catmull-rom
127
docs/chapters/catmullconv/catmull-rom.js
Normal file
@@ -0,0 +1,127 @@
|
||||
let points, knots;
|
||||
|
||||
setup() {
|
||||
points = [
|
||||
{x:38,y:136},
|
||||
{x:65,y:89},
|
||||
{x:99,y:178},
|
||||
{x:149,y:93},
|
||||
{x:191,y:163},
|
||||
{x:227,y:122},
|
||||
{x:251,y:132}
|
||||
];
|
||||
setMovable(points);
|
||||
knots = [0, 1/3, 2/3, 1];
|
||||
setSlider(`.slide-control.tension`, `tension`, 0.5);
|
||||
}
|
||||
|
||||
onTension(v) {
|
||||
if (v < 0.5) {
|
||||
v = map(v,0.5,0,1,4);
|
||||
v = 1/v;
|
||||
} else {
|
||||
v = map(v,0.5,1,1,4);
|
||||
}
|
||||
this.tension = v;
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
|
||||
const [first, last] = this.generateVirtualPoints();
|
||||
const full = [first, ...points, last];
|
||||
|
||||
for (let i=0, e=full.length-3; i<e; i++) {
|
||||
this.dragSegment(
|
||||
full[i],
|
||||
full[i+1],
|
||||
full[i+2],
|
||||
full[i+3]
|
||||
);
|
||||
}
|
||||
|
||||
points.forEach(p => {
|
||||
setColor( randomColor() );
|
||||
circle(p.x, p.y, 3);
|
||||
});
|
||||
}
|
||||
|
||||
generateVirtualPoints() {
|
||||
// see http://www.sdmath.com/math/geometry/reflection_across_line.html#formulasmb
|
||||
const n = points.length,
|
||||
p1 = points[0],
|
||||
p2 = points[1],
|
||||
p3 = points[n-2],
|
||||
p4 = points[n-1],
|
||||
m = (p4.y-p1.y)/(p4.x-p1.x),
|
||||
b = (p4.x*p1.y-p1.x*p4.y)/(p4.x-p1.x),
|
||||
ratio = 0.5;
|
||||
|
||||
return [[p2,p1], [p3,p4]].map(pair => {
|
||||
const p = pair[0],
|
||||
M = pair[1],
|
||||
reflected = {
|
||||
x: M.x - (p.x - M.x),
|
||||
y: M.y - (p.y - M.y),
|
||||
},
|
||||
projected = {
|
||||
x: ((1 - m**2)*p.x + 2*m*p.y - 2*m*b) / (m**2 + 1),
|
||||
y: ((m**2 - 1)*p.y + 2*m*p.x + 2*b) / (m**2 + 1)
|
||||
};
|
||||
return {
|
||||
x: (1-ratio) * reflected.x + ratio * projected.x,
|
||||
y: (1-ratio) * reflected.y + ratio * projected.y
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
dragSegment(p0, p1, p2, p3) {
|
||||
const alpha = 0.5,
|
||||
t0 = 0,
|
||||
// See https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline#Definition
|
||||
t1 = t0 + ((p1.x-p0.x)**2 + (p1.y-p0.y)**2)**alpha,
|
||||
t2 = t1 + ((p2.x-p1.x)**2 + (p2.y-p1.y)**2)**alpha,
|
||||
t3 = t2 + ((p3.x-p2.x)**2 + (p3.y-p2.y)**2)**alpha,
|
||||
s = (t2 - t1) / this.tension,
|
||||
// See https://stackoverflow.com/a/23980479/740553
|
||||
tangent1 = {
|
||||
x: s * ((p1.x-p0.x)/(t1-t0) - (p2.x-p0.x)/(t2-t0) + (p2.x-p1.x)/(t2-t1)),
|
||||
y: s * ((p1.y-p0.y)/(t1-t0) - (p2.y-p0.y)/(t2-t0) + (p2.y-p1.y)/(t2-t1))
|
||||
},
|
||||
tangent2 = {
|
||||
x: s * ((p2.x-p1.x)/(t2-t1) - (p3.x-p1.x)/(t3-t1) + (p3.x-p2.x)/(t3-t2)),
|
||||
y: s * ((p2.y-p1.y)/(t2-t1) - (p3.y-p1.y)/(t3-t1) + (p3.y-p2.y)/(t3-t2))
|
||||
};
|
||||
|
||||
noFill();
|
||||
setStroke( randomColor() );
|
||||
|
||||
start();
|
||||
this.markCoordinate(0, p1,p2,tangent1,tangent2);
|
||||
for(let s=0.01, t=s; t<1; t+=0.01) this.markCoordinate(t, p1,p2,tangent1,tangent2);
|
||||
this.markCoordinate(1, p1,p2,tangent1,tangent2);
|
||||
end();
|
||||
}
|
||||
|
||||
markCoordinate(t, p0, p1, m0, m1) {
|
||||
let c = 2*t**3 - 3*t**2,
|
||||
c0 = c + 1,
|
||||
c1 = t**3 - 2*t**2 + t,
|
||||
c2 = -c,
|
||||
c3 = t**3 - t**2,
|
||||
point = {
|
||||
x: c0 * p0.x + c1 * m0.x + c2 * p1.x + c3 * m1.x,
|
||||
y: c0 * p0.y + c1 * m0.y + c2 * p1.y + c3 * m1.y
|
||||
};
|
||||
vertex(point.x, point.y);
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
if (!this.currentPoint) {
|
||||
let {x, y} = this.cursor;
|
||||
points.push({ x, y });
|
||||
resetMovable(points);
|
||||
console.log(JSON.stringify(points))
|
||||
redraw();
|
||||
}
|
||||
}
|
@@ -1,12 +1,20 @@
|
||||
# Bézier curves and Catmull-Rom curves
|
||||
|
||||
Taking an excursion to different splines, the other common design curve is the [Catmull-Rom spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline). Now, a Catmull-Rom spline is a form of [cubic Hermite spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline), and as it so happens the cubic Bézier curve is _also_ a cubic Hermite spline, so maybe... maybe we can convert one into the other, and back, with some simple substitutions?
|
||||
Taking an excursion to different splines, the other common design curve is the [Catmull-Rom spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline), which unlike Bézier curves pass _through_ the points you define. In fact, let's start with just playing with one: the following graphic has a predefined curve that you manipulate the points for, or you can click/tap somewhere to extend the curve.
|
||||
|
||||
Unlike Bézier curves, Catmull-Rom splines pass through each point used to define the curve, except the first and last, which makes sense if you read the "natural language" description for how a Catmull-Rom spline works: a Catmull-Rom spline is a curve that, at each point P<sub>x</sub>, has a tangent along the line P<sub>x-1</sub> to P<sub>x+1</sub>. The curve runs from points P<sub>2</sub> to P<sub>n-1</sub>, and has a "tension" that determines how fast the curve passes through each point. The lower the tension, the faster the curve goes through each point, and the bigger its local tangent is.
|
||||
<graphics-element title="A Catmull-Rom curve" src="./catmull-rom.js">
|
||||
<input type="range" min="0.1" max="1" step="0.01" value="0.5" class="slide-control tension">
|
||||
</graphics-element>
|
||||
|
||||
I'll be showing the conversion to and from Catmull-Rom curves for the tension that the Processing language uses for its Catmull-Rom algorithm.
|
||||
You may have noticed the slider that seems to control the "tension" of the curve: that's a feature of Catmull-Rom curves; because Catmull-Rom curves pass through points, the curve tightness is controlled by a tension factor, rather than by moving control points around.
|
||||
|
||||
We start with showing the Catmull-Rom matrix form, which looks similar to the Bézier matrix form, with slightly different values in the matrix:
|
||||
What you may _also_ have noticed is that the Catmull-Rom curve seems to just go on forever: add as many points as you like, same curve, rigth? Well, sort of. Catmull-Rom curves are splines, a type of curve with arbitrary number of points, technically consisting of "segments between sets of points", but with maths that makes all the segments line up neatly. As such, at its core a Catmull-Rom consists of two points, and draws a curve between them based on the tangents at those points.
|
||||
|
||||
Now, a Catmull-Rom spline is a form of [cubic Hermite spline](https://en.wikipedia.org/wiki/Cubic_Hermite_spline), and as it so happens, the cubic Bézier curve is _also_ a cubic Hermite spline, so in an interesting bit of programming maths: we can losslessly convert between the two, and the maths (well, the final maths) is surprisingly simple!
|
||||
|
||||
The main difference between Catmull-Rom curves and Bezier curves is "what the points mean". A cubic Bézier curve is defined by a start point, a control point that implies the tangent at the start, a control point that implies the tangent at the end, and an end point. A Catmull-Rom curve is defined by a start point, a tangent that for that starting point, an end point, and a tangent for that end point. Those are _very_ similar, so let's see exactly _how_ similar they are.
|
||||
|
||||
We start with the matrix form of thee Catmull-Rom curve, which looks similar to the Bézier matrix form, with slightly different values in the matrix, and a slightly different coordinate vector:
|
||||
|
||||
\[
|
||||
CatmullRom(t) =
|
||||
@@ -26,11 +34,7 @@ We start with showing the Catmull-Rom matrix form, which looks similar to the B
|
||||
\end{bmatrix}
|
||||
\]
|
||||
|
||||
However, there's something funny going on here: the coordinate column matrix looks weird. The reason is that Catmull-Rom curves are actually curve segments that are described by two coordinate points, and two tangents; the curve starts at coordinate V1, and ends at coordinate V2, with the curve "departing" V1 with a tangent vector V'1 and "arriving" at V2 with tangent vector V'2.
|
||||
|
||||
This is not particularly useful if we want to draw Catmull-Rom curves in the same way we draw Bézier curves, i.e. by providing four points. However, we can fairly easily go from the former to the latter, but it's going to require some linear algebra, so if you just want to know how to convert between the two coordinate systems: skip the following bit.
|
||||
|
||||
But... if you want to know <em>why</em> that conversion works, let's do some maths!
|
||||
So the question is: how can we convert that expression with Catmull-Rom matrix and vector into an expression that uses Bézier matrix and vector? The short answer is of course "by using linear algebra", but the real answer is a bit longer, and involves some maths that you may not even care for: just skip over the next bit to see the incredibly simple conversions between the formats, but if you want to know _why_... let's go!
|
||||
|
||||
<div class="note">
|
||||
|
||||
@@ -539,14 +543,14 @@ Similarly, if we have a Bézier curve defined by four coordinates P<sub>1</sub>
|
||||
\end{bmatrix}_{Bézier}
|
||||
\Rightarrow
|
||||
\begin{bmatrix}
|
||||
P_4 + 6(P_1 - P_2) \\
|
||||
P_1 \\
|
||||
P_4 \\
|
||||
P_1 + 6(P_4 - P_3)
|
||||
P_4 + 3(P_1 - P_2) \\
|
||||
P_1 + 3(P_4 - P_3)
|
||||
\end{bmatrix}_{CatmullRom}
|
||||
\]
|
||||
|
||||
or, if your API requires specifying Catmull-Rom curves using "point + tangent" form:
|
||||
Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:
|
||||
|
||||
\[
|
||||
\begin{bmatrix}
|
||||
@@ -557,9 +561,9 @@ or, if your API requires specifying Catmull-Rom curves using "point + tangent" f
|
||||
\end{bmatrix}_{Bézier}
|
||||
\Rightarrow
|
||||
\begin{bmatrix}
|
||||
P_4 + 6(P_1 - P_2) \\
|
||||
P_1 \\
|
||||
P_4 \\
|
||||
P_4 + 3(P_1 - P_2) \\
|
||||
P_1 + 3(P_4 - P_3)
|
||||
P_1 + 6(P_4 - P_3)
|
||||
\end{bmatrix}_{CatmullRom}
|
||||
\]
|
||||
|
@@ -1,148 +0,0 @@
|
||||
import invert from "./matrix-invert.js";
|
||||
|
||||
var binomialCoefficients = [[1],[1,1]];
|
||||
|
||||
function binomial(n,k) {
|
||||
if (n===0) return 1;
|
||||
var lut = binomialCoefficients;
|
||||
while(n >= lut.length) {
|
||||
var s = lut.length;
|
||||
var nextRow = [1];
|
||||
for(var i=1,prev=s-1; i<s; i++) {
|
||||
nextRow[i] = lut[prev][i-1] + lut[prev][i];
|
||||
}
|
||||
nextRow[s] = 1;
|
||||
lut.push(nextRow);
|
||||
}
|
||||
return lut[n][k];
|
||||
}
|
||||
|
||||
function transpose(M) {
|
||||
var Mt = [];
|
||||
M.forEach(row => Mt.push([]));
|
||||
M.forEach((row,r) => row.forEach((v,c) => Mt[c][r] = v));
|
||||
return Mt;
|
||||
}
|
||||
|
||||
function row(M,i) {
|
||||
return M[i];
|
||||
}
|
||||
|
||||
function col(M,i) {
|
||||
var col = [];
|
||||
for(var r=0, l=M.length; r<l; r++) {
|
||||
col.push(M[r][i]);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
function multiply(M1, M2) {
|
||||
// prep
|
||||
var M = [];
|
||||
var dims = [M1.length, M1[0].length, M2.length, M2[0].length];
|
||||
// work
|
||||
for (var r=0, c; r<dims[0]; r++) {
|
||||
M[r] = [];
|
||||
var _row = row(M1, r);
|
||||
for (c=0; c<dims[3]; c++) {
|
||||
var _col = col(M2,c);
|
||||
var reducer = (a,v,i) => a + _col[i]*v;
|
||||
M[r][c] = _row.reduce(reducer, 0);
|
||||
}
|
||||
}
|
||||
return M;
|
||||
}
|
||||
|
||||
function getValueColumn(P, prop) {
|
||||
var col = [];
|
||||
P.forEach(v => col.push([v[prop]]));
|
||||
return col;
|
||||
}
|
||||
|
||||
function computeBasisMatrix(n) {
|
||||
/*
|
||||
We can form any basis matrix using a generative approach:
|
||||
|
||||
- it's an M = (n x n) matrix
|
||||
- it's a lower triangular matrix: all the entries above the main diagonal are zero
|
||||
- the main diagonal consists of the binomial coefficients for n
|
||||
- all entries are symmetric about the antidiagonal.
|
||||
|
||||
What's more, if we number rows and columns starting at 0, then
|
||||
the value at position M[r,c], with row=r and column=c, can be
|
||||
expressed as:
|
||||
|
||||
M[r,c] = (r choose c) * M[r,r] * S,
|
||||
|
||||
where S = 1 if r+c is even, or -1 otherwise
|
||||
|
||||
That is: the values in column c are directly computed off of the
|
||||
binomial coefficients on the main diagonal, through multiplication
|
||||
by a binomial based on matrix position, with the sign of the value
|
||||
also determined by matrix position. This is actually very easy to
|
||||
write out in code:
|
||||
*/
|
||||
|
||||
// form the square matrix, and set it to all zeroes
|
||||
var M = [], i = n;
|
||||
while (i--) { M[i] = "0".repeat(n).split('').map(v => parseInt(v)); }
|
||||
|
||||
// populate the main diagonal
|
||||
var k = n - 1;
|
||||
for (i=0; i<n; i++) { M[i][i] = binomial(k, i); }
|
||||
|
||||
// compute the remaining values
|
||||
for (var c=0, r; c<n; c++) {
|
||||
for (r=c+1; r<n; r++) {
|
||||
var sign = (r+c)%2 ? -1 : 1;
|
||||
var value = binomial(r, c) * M[r][r];
|
||||
M[r][c] = sign * value; }}
|
||||
|
||||
return M;
|
||||
}
|
||||
|
||||
function raiseRowPower(row, i) {
|
||||
return row.map(v => Math.pow(v,i));
|
||||
}
|
||||
|
||||
function formTMatrix(S, n) {
|
||||
n = n || S.length;
|
||||
var Tp = [];
|
||||
// it's easier to generate the transposed matrix:
|
||||
for(var i=0; i<n; i++) Tp.push( raiseRowPower(S, i));
|
||||
return {
|
||||
Tt: Tp,
|
||||
T: transpose(Tp) // and then transpose "again" to get the real matrix
|
||||
};
|
||||
}
|
||||
|
||||
function computeBestFit(P, M, S, n) {
|
||||
n = n || P.length;
|
||||
var tm = formTMatrix(S, n),
|
||||
T = tm.T,
|
||||
Tt = tm.Tt,
|
||||
M1 = invert(M),
|
||||
TtT1 = invert(multiply(Tt,T)),
|
||||
step1 = multiply(TtT1, Tt),
|
||||
step2 = multiply(M1, step1),
|
||||
X = getValueColumn(P,'x'),
|
||||
Cx = multiply(step2, X),
|
||||
Y = getValueColumn(P,'y'),
|
||||
Cy = multiply(step2, Y);
|
||||
return { x: Cx, y: Cy };
|
||||
}
|
||||
|
||||
function fit(points, tvalues) {
|
||||
// mode could be an int index to fit.modes, below,
|
||||
// which are used to abstract time values, OR it
|
||||
// could be a prespecified array of time values to
|
||||
// be used in the final curve fitting step.
|
||||
const n = points.length,
|
||||
P = Array.from(points),
|
||||
M = computeBasisMatrix(n),
|
||||
S = tvalues,
|
||||
C = computeBestFit(P, M, S, n);
|
||||
return { n, P, M, S, C };
|
||||
}
|
||||
|
||||
export default fit;
|
@@ -1,5 +1,3 @@
|
||||
import fit from "./curve-fitter.js";
|
||||
|
||||
let points = [], curve, sliders;
|
||||
|
||||
setup() {
|
||||
@@ -24,29 +22,90 @@ draw() {
|
||||
setColor('black');
|
||||
setFontSize(16);
|
||||
setTextStroke(`white`, 4);
|
||||
if (points.length > 2) {
|
||||
curve = this.fitCurve(points);
|
||||
const n = points.length;
|
||||
if (n > 2 && sliders && sliders.values) {
|
||||
curve = this.fitCurveToPoints(n);
|
||||
curve.drawSkeleton(`blue`);
|
||||
curve.drawCurve();
|
||||
|
||||
text(this.label, this.width/2, 20, CENTER);
|
||||
}
|
||||
|
||||
points.forEach(p => circle(p.x, p.y, 3));
|
||||
}
|
||||
|
||||
fitCurve(points) {
|
||||
let n = points.length;
|
||||
let tvalues = sliders ? sliders.values : [...new Array(n)].map((_,i) =>i/(n-1));
|
||||
let bestFitData = fit(points, tvalues),
|
||||
x = bestFitData.C.x,
|
||||
y = bestFitData.C.y,
|
||||
bpoints = x.map((r,i) => (
|
||||
{x: r[0], y: y[i][0]}
|
||||
));
|
||||
fitCurveToPoints(n) {
|
||||
// alright, let's do this thing:
|
||||
const tm = this.formTMatrix(sliders.values, n),
|
||||
T = tm.T,
|
||||
Tt = tm.Tt,
|
||||
M = this.generateBasisMatrix(n),
|
||||
M1 = M.invert(),
|
||||
TtT1 = Tt.multiply(T).invert(),
|
||||
step1 = TtT1.multiply(Tt),
|
||||
step2 = M1.multiply(step1),
|
||||
// almost there...
|
||||
X = new Matrix(points.map((v) => [v.x])),
|
||||
Cx = step2.multiply(X),
|
||||
x = Cx.data,
|
||||
// almost...
|
||||
Y = new Matrix(points.map((v) => [v.y])),
|
||||
Cy = step2.multiply(Y),
|
||||
y = Cy.data,
|
||||
// last step!
|
||||
bpoints = x.map((r,i) => ({x: r[0], y: y[i][0]}));
|
||||
|
||||
return new Bezier(this, bpoints);
|
||||
}
|
||||
|
||||
formTMatrix(row, n) {
|
||||
// it's actually easier to create the transposed
|
||||
// version, and then (un)transpose that to get T!
|
||||
let data = [];
|
||||
for (var i = 0; i < n; i++) {
|
||||
data.push(row.map((v) => v ** i));
|
||||
}
|
||||
const Tt = new Matrix(n, n, data);
|
||||
const T = Tt.transpose();
|
||||
return { T, Tt };
|
||||
}
|
||||
|
||||
generateBasisMatrix(n) {
|
||||
const M = new Matrix(n, n);
|
||||
|
||||
// populate the main diagonal
|
||||
var k = n - 1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
M.set(i, i, binomial(k, i));
|
||||
}
|
||||
|
||||
// compute the remaining values
|
||||
for (var c = 0, r; c < n; c++) {
|
||||
for (r = c + 1; r < n; r++) {
|
||||
var sign = (r + c) % 2 === 0 ? 1 : -1;
|
||||
var value = binomial(r, c) * M.get(r, r);
|
||||
M.set(r, c, sign * value);
|
||||
}
|
||||
}
|
||||
|
||||
return M;
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
if (!this.currentPoint) {
|
||||
const {x, y} = this.cursor;
|
||||
points.push({ x, y });
|
||||
resetMovable(points);
|
||||
this.updateSliders();
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------
|
||||
// The rest of this code is slider logic
|
||||
// -------------------------------------
|
||||
|
||||
|
||||
updateSliders() {
|
||||
if (sliders && points.length > 2) {
|
||||
sliders.innerHTML = ``;
|
||||
@@ -101,13 +160,3 @@ setSliderValues(mode) {
|
||||
s.value = sliders.values[i];
|
||||
});
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
if (!this.currentPoint) {
|
||||
const {x, y} = this.cursor;
|
||||
points.push({ x, y });
|
||||
resetMovable(points);
|
||||
this.updateSliders();
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
|
@@ -1,110 +0,0 @@
|
||||
// Copied from http://blog.acipo.com/matrix-inversion-in-javascript/
|
||||
|
||||
// Returns the inverse of matrix `M`.
|
||||
export default function matrix_invert(M) {
|
||||
// I use Guassian Elimination to calculate the inverse:
|
||||
// (1) 'augment' the matrix (left) by the identity (on the right)
|
||||
// (2) Turn the matrix on the left into the identity by elemetry row ops
|
||||
// (3) The matrix on the right is the inverse (was the identity matrix)
|
||||
// There are 3 elemtary row ops: (I combine b and c in my code)
|
||||
// (a) Swap 2 rows
|
||||
// (b) Multiply a row by a scalar
|
||||
// (c) Add 2 rows
|
||||
|
||||
//if the matrix isn't square: exit (error)
|
||||
if (M.length !== M[0].length) {
|
||||
console.log('not square');
|
||||
return;
|
||||
}
|
||||
|
||||
//create the identity matrix (I), and a copy (C) of the original
|
||||
var i = 0,
|
||||
ii = 0,
|
||||
j = 0,
|
||||
dim = M.length,
|
||||
e = 0,
|
||||
t = 0;
|
||||
var I = [],
|
||||
C = [];
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// Create the row
|
||||
I[I.length] = [];
|
||||
C[C.length] = [];
|
||||
for (j = 0; j < dim; j += 1) {
|
||||
//if we're on the diagonal, put a 1 (for identity)
|
||||
if (i == j) {
|
||||
I[i][j] = 1;
|
||||
} else {
|
||||
I[i][j] = 0;
|
||||
}
|
||||
|
||||
// Also, make the copy of the original
|
||||
C[i][j] = M[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform elementary row operations
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// get the element e on the diagonal
|
||||
e = C[i][i];
|
||||
|
||||
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
|
||||
if (e == 0) {
|
||||
//look through every row below the i'th row
|
||||
for (ii = i + 1; ii < dim; ii += 1) {
|
||||
//if the ii'th row has a non-0 in the i'th col
|
||||
if (C[ii][i] != 0) {
|
||||
//it would make the diagonal have a non-0 so swap it
|
||||
for (j = 0; j < dim; j++) {
|
||||
e = C[i][j]; //temp store i'th row
|
||||
C[i][j] = C[ii][j]; //replace i'th row by ii'th
|
||||
C[ii][j] = e; //repace ii'th by temp
|
||||
e = I[i][j]; //temp store i'th row
|
||||
I[i][j] = I[ii][j]; //replace i'th row by ii'th
|
||||
I[ii][j] = e; //repace ii'th by temp
|
||||
}
|
||||
//don't bother checking other rows since we've swapped
|
||||
break;
|
||||
}
|
||||
}
|
||||
//get the new diagonal
|
||||
e = C[i][i];
|
||||
//if it's still 0, not invertable (error)
|
||||
if (e == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale this row down by e (so we have a 1 on the diagonal)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[i][j] = C[i][j] / e; //apply to original matrix
|
||||
I[i][j] = I[i][j] / e; //apply to identity
|
||||
}
|
||||
|
||||
// Subtract this row (scaled appropriately for each row) from ALL of
|
||||
// the other rows so that there will be 0's in this column in the
|
||||
// rows above and below this one
|
||||
for (ii = 0; ii < dim; ii++) {
|
||||
// Only apply to other rows (we want a 1 on the diagonal)
|
||||
if (ii == i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to change this element to 0
|
||||
e = C[ii][i];
|
||||
|
||||
// Subtract (the row above(or below) scaled by e) from (the
|
||||
// current row) but start at the i'th column and assume all the
|
||||
// stuff left of diagonal is 0 (which it should be if we made this
|
||||
// algorithm correctly)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[ii][j] -= e * C[i][j]; //apply to original matrix
|
||||
I[ii][j] -= e * I[i][j]; //apply to identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we've done all operations, C should be the identity
|
||||
//matrix I should be the inverse:
|
||||
return I;
|
||||
};
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
257
docs/index.html
@@ -197,13 +197,15 @@
|
||||
<div class="figure">
|
||||
<graphics-element title="A quadratic Bézier curve" width="275" height="275" src="./chapters/introduction/quadratic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\54e9ec0600ac436b0e6f0c6b5005cf03.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="A cubic Bézier curve" width="275" height="275" src="./chapters/introduction/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\8d158a13e9a86969b99c64057644cbc6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A cubic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -219,8 +221,9 @@
|
||||
<p>So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over those lines gives us two points, between which we can again perform linear interpolation, yielding a single point. And that point —and all points we can form in this way for all ratios taken together— form our Bézier curve:</p>
|
||||
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" height="275" src="./chapters/whatis/interpolation.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\9b3633889c38325c24c19ce18ab94ad6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -246,8 +249,9 @@
|
||||
<p>So, parametric curves don't define a <i>y</i> coordinate in terms of an <i>x</i> coordinate, like normal functions do, but they instead link the values to a "control" variable. If we vary the value of <i>t</i>, then with every change we get <strong>two</strong> values, which we can use as (<i>x</i>,<i>y</i>) coordinates in a graph. The above set of functions, for instance, generates points on a circle: We can range <i>t</i> from negative to positive infinity, and the resulting (<i>x</i>,<i>y</i>) coordinates will always lie on a circle with radius 1 around the origin (0,0). If we plot it for <i>t</i> from 0 to 5, we get this:</p>
|
||||
<graphics-element title="A (partial) circle: x=sin(t), y=cos(t)" width="275" height="275" src="./chapters/explanation/circle.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\explanation\6d2f915735ebdc05e42c0ea7adc85343.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A (partial) circle: x=sin(t), y=cos(t)</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="10" step="0.1" value="5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -328,24 +332,27 @@ function Bezier(3,t):
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic interpolations" width="275" height="275" src="./chapters/control/lerp.js" data-degree="3">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\ecc15848fbe7b2176b0c89973f07c694.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic interpolations</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="Cubic interpolations" width="275" height="275" src="./chapters/control/lerp.js" data-degree="4">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\49423783987ac4bc49fbe4c519dbc1d1.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic interpolations</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="15th degree interpolations" width="275" height="275" src="./chapters/control/lerp.js" data-degree="15">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\afd21a9ba16965c2e7ec2d0d14892250.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>15th degree interpolations</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -359,8 +366,9 @@ function Bezier(3,t):
|
||||
<p>Which gives us the curve we saw at the top of the article:</p>
|
||||
<graphics-element title="Our cubic Bézier curve" width="275" height="275" src="./chapters/control/../introduction/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\8d158a13e9a86969b99c64057644cbc6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Our cubic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>What else can we do with Bézier curves? Quite a lot, actually. The rest of this article covers a multitude of possible operations and algorithms that we can apply, and the tasks they achieve.</p>
|
||||
@@ -404,8 +412,9 @@ function Bezier(3,t,w[]):
|
||||
<p>But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:</p>
|
||||
<graphics-element title="Our rational cubic Bézier curve" width="275" height="275" src="./chapters/weightcontrol/rational.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\weightcontrol\0ef06657f0540938686b9b35b249a22b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Our rational cubic Bézier curve</label>
|
||||
</fallback-image>
|
||||
ratio 1 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
ratio 2 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
@@ -462,13 +471,15 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic infinite interval Bézier curve" width="275" height="275" src="./chapters/extended/extended.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\extended\391a61142c56b79260680aefb08cd9c4.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic infinite interval Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic infinite interval Bézier curve" width="275" height="275" src="./chapters/extended/extended.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\extended\baeceec6e1587794b8b275a90d5d85e9.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic infinite interval Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -519,8 +530,9 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>To see this in action, mouse-over the following sketch. Moving the mouse changes which curve point is explicitly evaluated using de Casteljau's algorithm, moving the cursor left-to-right (or, of course, right-to-left), shows you how a curve is generated using this approach.</p>
|
||||
<graphics-element title="Traversing a curve using de Casteljau's algorithm" width="275" height="275" src="./chapters/decasteljau/decasteljau.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\decasteljau\715d1d2eecc762d6bc1470954b145018.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Traversing a curve using de Casteljau's algorithm</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -560,16 +572,18 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<div class="figure">
|
||||
<graphics-element title="Flattening a quadratic curve" width="275" height="275" src="./chapters/flattening/flatten.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\flattening\3deec756c96e53127cd1d615c61043ae.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Flattening a quadratic curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="16" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="Flattening a cubic curve" width="275" height="275" src="./chapters/flattening/flatten.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\flattening\e2bb7113d5cda2e3fd29bbc54fbe8841.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Flattening a cubic curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -604,8 +618,9 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>Using de Casteljau's algorithm, we can also find all the points we need to split up a Bézier curve into two, smaller curves, which taken together form the original curve. When we construct de Casteljau's skeleton for some value <code>t</code>, the procedure gives us all the points we need to split a curve at that <code>t</code> value: one curve is defined by all the inside skeleton points found prior to our on-curve point, with the other curve being defined by all the inside skeleton points after our on-curve point.</p>
|
||||
<graphics-element title="Splitting a curve" width="825" height="275" src="./chapters/splitting/splitting.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\bef2f09698c0d3d2b7c4c031be17ff69.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -727,8 +742,9 @@ function drawCurve(points[], t):
|
||||
<p>And we're done: we now have an expression that lets us approximate an <code>n+1</code><sup>th</sup> order curve with a lower <code>n</code><sup>th</sup> order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.</p>
|
||||
<graphics-element title="A variable-order Bézier curve" width="275" height="275" src="./chapters/reordering/reorder.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\reordering\387f931043aabd6c467985c568482636.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A variable-order Bézier curve</label>
|
||||
</fallback-image>
|
||||
<button class="raise">raise</button>
|
||||
<button class="lower">lower</button>
|
||||
@@ -802,13 +818,15 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier tangents and normals" width="275" height="275" src="./chapters/pointvectors/quadratic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointvectors\44d454f380ae84dbe4661bd67c406edc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic Bézier tangents and normals</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic Bézier tangents and normals" width="275" height="275" src="./chapters/pointvectors/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointvectors\58719bde12c1cbd535d5d8af1bef9c2b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic Bézier tangents and normals</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -836,8 +854,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/frenet.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\cbadec403b99dec015ab084ee10e1671.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -910,8 +929,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/rotation-minimizing.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\9983328e50c2156c124bb1ea4ad5c05b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -928,14 +948,16 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.</p>
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\components\008604bc4c53bd7e0d97c99a67812ad1.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\components\5214256129e6396e7ac1f1713fa9c88d.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1070,15 +1092,17 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:</p>
|
||||
<graphics-element title="Quadratic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\9850eec01924deae7fda4400ce44270d.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||||
<graphics-element title="Cubic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\890406c7bc96904224f8f14940bf3e56.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1095,13 +1119,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier bounding box" width="275" height="275" src="./chapters/boundingbox/bbox.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\boundingbox\e2c621442e98e2cd20af7efe1cfb041f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic Bézier bounding box</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic Bézier bounding box" width="275" height="275" src="./chapters/boundingbox/bbox.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\boundingbox\f8989a62ebec9d6f123291c146caab5b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic Bézier bounding box</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1123,13 +1149,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\b3ccd45a72c815388aee6515fe37a486.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\31655f24b7dd8b8871687b6610d9ac0e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1141,13 +1169,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="275" height="275" src="./chapters/tightbounds/tightbounds.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\tightbounds\ccc77ae1f57d7dd7ce4d5397fe1b140b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Aligning a quadratic curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="275" height="275" src="./chapters/tightbounds/tightbounds.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\tightbounds\419415bee6ffd7598c035c42de09a94f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Aligning a cubic curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1186,8 +1216,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>Taking that into account, we compute <em>t</em>, we disregard any <em>t</em> value that isn't in the Bézier interval [0,1], and we now know at which <em>t</em> value(s) our curve will inflect.</p>
|
||||
<graphics-element title="Finding cubic Bézier curve inflections" width="275" height="275" src="./chapters/inflections/inflection.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\inflections\9e1ce3975100600d4979370851929b73.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding cubic Bézier curve inflections</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1198,8 +1229,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
<graphics-element title="The canonical curve map" width="400" height="400" src="./chapters/canonical/canonical.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\675217e6df3d75c86503dc2af623e14e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>This is a fairly funky image, so let's see what the various parts of it mean...</p>
|
||||
@@ -1264,8 +1296,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:</p>
|
||||
<graphics-element title="A cubic curve mapped to canonical form" width="800" height="400" src="./chapters/canonical/interactive.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\344346f09b6d5d7e3ddf91084ad50d46.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1275,8 +1308,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\dd28d64458d22f4fe89c98568258efcb.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1302,8 +1336,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So the procedure is fairly straight forward: pick an <code>x</code>, find the associted <code>t</code> value, evaluate our curve <em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this <code>x</code>. Move the slider for the following graphic to see this in action:</p>
|
||||
<graphics-element title="Finding By(t), by finding t for a given x" width="275" height="275" src="./chapters/yforx/yforx.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\efcfe9b48ca4e65eef3d4bf3e4c97bc3.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding By(t), by finding t for a given x</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1324,18 +1359,21 @@ y = curve.get(t).y</code></pre>
|
||||
<div class="figure">
|
||||
<graphics-element title="A function's approximated integral" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="10">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\580c33f599b70de44b17c546098508aa.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A function's approximated integral</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="A better approximation" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="24">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\0d7138b99f5986332a050a8479eefa57.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A better approximation</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="An even better approximation" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="99">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\475547c773a7279dc037c9ced2c8c6dc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>An even better approximation</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1357,8 +1395,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>If we use the Legendre-Gauss values for our <em>C</em> values (thickness for each strip) and <em>t</em> values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?</p>
|
||||
<graphics-element title="Arc length for a Bézier curve" width="275" height="275" src="./chapters/arclength/arclength.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\195f9a3c60f8dfe977c6450d21968f69.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Arc length for a Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1370,16 +1409,18 @@ y = curve.get(t).y</code></pre>
|
||||
|
||||
<graphics-element title="Approximate quadratic curve arc length" width="275" height="275" src="./chapters/arclengthapprox/approximate.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclengthapprox\a040f6b7c7c33ada25ecfd1060726545.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Approximate quadratic curve arc length</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="24" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="Approximate cubic curve arc length" width="275" height="275" src="./chapters/arclengthapprox/approximate.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclengthapprox\c270144cc41e4ebc4b0b2331473530fa.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Approximate cubic curve arc length</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="32" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1425,8 +1466,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.</p>
|
||||
<graphics-element title="Matching curvatures for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\5fcfb0572cae06717506c84768aa568c.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>One thing you may have noticed in this sketch is that sometimes the curvature looks fine, but seems to be pointing in the wrong direction, making it hard to line up the curves properly. A way around that, of course, is to show the curvature on both sides of the curve, so let's just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:</p>
|
||||
@@ -1434,8 +1476,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:</p>
|
||||
<graphics-element title="(Easier) curvature matching for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" data-omni="true">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\876d7b2750d7c29068ac6181c3634d25.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1449,16 +1492,18 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\52f815cefe99dabc47ca83d0b97b61fc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>So, how do we "cut up" the arc length function at regular intervals, when we can't really work with it? We basically cheat: we run through the curve using <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and then we find "the closest <code>t</code> value that matches some required distance" using those values instead. If we have a low number of points sampled, we can then even refine which <code>t</code> value "should" work for our desired distance by interpolating between two points, but if we have a high enough number of samples, we don't even need to bother.</p>
|
||||
<p>So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\133bf9d02801a3149c9ddb8b313e6797.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1476,8 +1521,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).</p>
|
||||
<graphics-element title="Line/line intersections" width="275" height="275" src="./chapters/intersections/line-line.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\61876a2bd727df377619c5ad34ce86be.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Line/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<div class="howtocode">
|
||||
@@ -1508,13 +1554,15 @@ lli = function(line1, line2):
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic curve/line intersections" width="275" height="275" src="./chapters/intersections/curve-line.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\594c2df534a1736c03cd3a96ff4a9913.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic curve/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic curve/line intersections" width="275" height="275" src="./chapters/intersections/curve-line.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\dc26a6063dadc31d242f1c1c8f38bb5e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic curve/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1540,8 +1588,9 @@ lli = function(line1, line2):
|
||||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||||
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\3689ed0c15eace45a1f6ae03909ad8ed.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
<button class="next">Advance one step</button>
|
||||
@@ -1561,15 +1610,17 @@ lli = function(line1, line2):
|
||||
|
||||
<graphics-element inline={true} title="Projections in a quadratic Bézier curve" width="275" height="275" src="./chapters/abc/abc.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\abc\7a69dd4350ddda5701712e1d3b46b863.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Projections in a quadratic Bézier curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
<graphics-element inline={true} title="Projections in a cubic Bézier curve" width="275" height="275" src="./chapters/abc/abc.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\abc\eeec7cf16fb22c666e0143a3a030731f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Projections in a cubic Bézier curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1615,16 +1666,18 @@ lli = function(line1, line2):
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\067a3df30e32708fc0d13f8eb78c0b05.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Fitting a quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\173ea31517a72a927d561f121f0677db.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding a circle through three points</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<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>
|
||||
@@ -1636,15 +1689,17 @@ lli = function(line1, line2):
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\8d045d352f5017b65e60620b92d7ae29.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding the cubic e₁ and e₂ given three points </label>
|
||||
</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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\eab6ea46fa93030e03ec0ef7deb571dc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Fitting a quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>That looks perfectly servicable!</p>
|
||||
@@ -1672,8 +1727,9 @@ for (coordinate, index) in LUT:
|
||||
<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" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1687,24 +1743,27 @@ for (coordinate, index) in LUT:
|
||||
<p>And we're done, because that's our new quadratic control point!</p>
|
||||
<graphics-element title="Molding a quadratic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\6b91671c2962530b863ae0da5789a9cc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>As before, cubic curves are a bit more work, because while it's easy to find our initial <code>t</code> value and ABC values, getting those all-important <code>e1</code> and <code>e2</code> coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate <code>t</code> value ourselves, which allowed us to find appropriate <code>e1</code> and <code>e2</code> 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 <code>t</code> value, and its own <code>e1</code> and <code>e2</code> values, and those may not make sense for the rest of the curve.</p>
|
||||
<p>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 <code>t</code> value and <code>e1</code>/<code>e2</code> coordinates:</p>
|
||||
<graphics-element title="Molding a cubic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\522f1edd37163772b81acb86d3a4f423.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>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.</p>
|
||||
<p>One way to combat this might be to combine the above approach with the approach from the <a href="#pointcurves">creating curves</a> section: generate both the "unchanged <code>t</code>/<code>e1</code>/<code>e2</code>" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised <code>t</code> value, and then interpolating between those two curves:</p>
|
||||
<graphics-element title="Molding a cubic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="cubic" data-interpolated="true">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\ea1656c068a60631135ad499e8a29453.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="200" step="1" value="100" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1788,8 +1847,9 @@ for (coordinate, index) in LUT:
|
||||
<p>So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least three. You can click for more points, and the code will simply try to compute an exact fit using a Bézier curve of the appropriate order. Four points? Cubic Bézier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once there's enough points for compound <a href="https://en.wikipedia.org/wiki/Round-off_error#Floating-point_number_system">floating point rounding errors</a> to start making a difference (which is around 10~11 points).</p>
|
||||
<graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\curvefitting\c2c92587a184efa6c4fee45e4a3e32ed.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\curvefitting\c6c8442e24793ce72a872ce29b2b4125.png">
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<button class="toggle">toggle</button>
|
||||
<div class="sliders"><!-- this will contain range inputs, created by the graphic --></div>
|
||||
@@ -1800,83 +1860,92 @@ for (coordinate, index) in LUT:
|
||||
</section>
|
||||
<section id="catmullconv">
|
||||
<h1><a href="#catmullconv">Bézier curves and Catmull-Rom curves</a></h1>
|
||||
<p>Taking an excursion to different splines, the other common design curve is the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>. Now, a Catmull-Rom spline is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as it so happens the cubic Bézier curve is <em>also</em> a cubic Hermite spline, so maybe... maybe we can convert one into the other, and back, with some simple substitutions?</p>
|
||||
<p>Unlike Bézier curves, Catmull-Rom splines pass through each point used to define the curve, except the first and last, which makes sense if you read the "natural language" description for how a Catmull-Rom spline works: a Catmull-Rom spline is a curve that, at each point P<sub>x</sub>, has a tangent along the line P<sub>x-1</sub> to P<sub>x+1</sub>. The curve runs from points P<sub>2</sub> to P<sub>n-1</sub>, and has a "tension" that determines how fast the curve passes through each point. The lower the tension, the faster the curve goes through each point, and the bigger its local tangent is.</p>
|
||||
<p>I'll be showing the conversion to and from Catmull-Rom curves for the tension that the Processing language uses for its Catmull-Rom algorithm.</p>
|
||||
<p>We start with showing the Catmull-Rom matrix form, which looks similar to the Bézier matrix form, with slightly different values in the matrix:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg" width="409px" height="75px" loading="lazy">
|
||||
<p>However, there's something funny going on here: the coordinate column matrix looks weird. The reason is that Catmull-Rom curves are actually curve segments that are described by two coordinate points, and two tangents; the curve starts at coordinate V1, and ends at coordinate V2, with the curve "departing" V1 with a tangent vector V'1 and "arriving" at V2 with tangent vector V'2.</p>
|
||||
<p>This is not particularly useful if we want to draw Catmull-Rom curves in the same way we draw Bézier curves, i.e. by providing four points. However, we can fairly easily go from the former to the latter, but it's going to require some linear algebra, so if you just want to know how to convert between the two coordinate systems: skip the following bit.</p>
|
||||
<p>But... if you want to know <em>why</em> that conversion works, let's do some maths!</p>
|
||||
<p>Taking an excursion to different splines, the other common design curve is the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>, which unlike Bézier curves pass <em>through</em> the points you define. In fact, let's start with just playing with one: the following graphic has a predefined curve that you manipulate the points for, or you can click/tap somewhere to extend the curve.</p>
|
||||
<graphics-element title="A Catmull-Rom curve" width="275" height="275" src="./chapters/catmullconv/catmull-rom.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\catmullconv\b0cb0cccdbea86dcdd610a871e86183f.png">
|
||||
<label>A Catmull-Rom curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0.1" max="1" step="0.01" value="0.5" class="slide-control tension">
|
||||
</graphics-element>
|
||||
|
||||
<p>You may have noticed the slider that seems to control the "tension" of the curve: that's a feature of Catmull-Rom curves; because Catmull-Rom curves pass through points, the curve tightness is controlled by a tension factor, rather than by moving control points around.</p>
|
||||
<p>What you may <em>also</em> have noticed is that the Catmull-Rom curve seems to just go on forever: add as many points as you like, same curve, rigth? Well, sort of. Catmull-Rom curves are splines, a type of curve with arbitrary number of points, technically consisting of "segments between sets of points", but with maths that makes all the segments line up neatly. As such, at its core a Catmull-Rom consists of two points, and draws a curve between them based on the tangents at those points.</p>
|
||||
<p>Now, a Catmull-Rom spline is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as it so happens, the cubic Bézier curve is <em>also</em> a cubic Hermite spline, so in an interesting bit of programming maths: we can losslessly convert between the two, and the maths (well, the final maths) is surprisingly simple!</p>
|
||||
<p>The main difference between Catmull-Rom curves and Bezier curves is "what the points mean". A cubic Bézier curve is defined by a start point, a control point that implies the tangent at the start, a control point that implies the tangent at the end, and an end point. A Catmull-Rom curve is defined by a start point, a tangent that for that starting point, an end point, and a tangent for that end point. Those are <em>very</em> similar, so let's see exactly <em>how</em> similar they are.</p>
|
||||
<p>We start with the matrix form of thee Catmull-Rom curve, which looks similar to the Bézier matrix form, with slightly different values in the matrix, and a slightly different coordinate vector:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg" width="423px" height="75px" loading="lazy">
|
||||
<p>So the question is: how can we convert that expression with Catmull-Rom matrix and vector into an expression that uses Bézier matrix and vector? The short answer is of course "by using linear algebra", but the real answer is a bit longer, and involves some maths that you may not even care for: just skip over the next bit to see the incredibly simple conversions between the formats, but if you want to know <em>why</em>... let's go!</p>
|
||||
<div class="note">
|
||||
|
||||
<h2>Deriving the conversion formulae</h2>
|
||||
<p>In order to convert between Catmull-Rom curves and Bézier curves, we need to know two things. Firstly, how to express the Catmull-Rom curve using a "set of four coordinates", rather than a mix of coordinates and tangents, and secondly, how to convert those Catmull-Rom coordinates to and from Bézier form.</p>
|
||||
<p>So, let's start with the first, where we want to satisfy the following equality:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg" width="187px" height="83px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg" width="196px" height="79px" loading="lazy">
|
||||
<p>This mapping says that in order to map a Catmull-Rom "point + tangent" vector to something based on an "all coordinates" vector, we need to determine the mapping matrix such that applying <em>T</em> yields P2 as start point, P3 as end point, and two tangents based on the lines between P1 and P3, and P2 nd P4, respectively.</p>
|
||||
<p>Computing <em>T</em> is really more "arranging the numbers":</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg" width="591px" height="83px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg" width="621px" height="79px" loading="lazy">
|
||||
<p>Thus:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg" width="143px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg" width="145px" height="76px" loading="lazy">
|
||||
<p>However, we're not <em>quite</em> done, because Catmull-Rom curves have a parameter called "tension", written as τ ("tau"), which is a scaling factor for the tangent vectors: the bigger the tension, the smaller the tangents, and the smaller the tension, the bigger the tangents. As such, the tension factor goes in the denominator for the tangents, and before we continue, let's add that tension factor into both our coordinate vector representation, and mapping matrix <em>T</em>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg" width="285px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg" width="291px" height="79px" loading="lazy">
|
||||
<p>With the mapping matrix properly done, let's rewrite the "point + tangent" Catmull-Rom matrix form to a matrix form in terms of four coordinates, and see what we end up with:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="409px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="423px" height="75px" loading="lazy">
|
||||
<p>Replace point/tangent vector with the expression for all-coordinates:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg" width="549px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg" width="564px" height="76px" loading="lazy">
|
||||
<p>and merge the matrices:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg" width="455px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg" width="465px" height="76px" loading="lazy">
|
||||
<p>This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg" width="353px" height="73px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg" width="359px" height="75px" loading="lazy">
|
||||
<p>So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg" width="227px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg" width="227px" height="76px" loading="lazy">
|
||||
<p>Into something that looks like this:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg" width="167px" height="73px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg" width="171px" height="75px" loading="lazy">
|
||||
<p>And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg" width="440px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg" width="452px" height="76px" loading="lazy">
|
||||
<p>Then we remove the coordinate vector from both sides without affecting the equality:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg" width="353px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg" width="356px" height="76px" loading="lazy">
|
||||
<p>Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg" width="657px" height="88px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg" width="672px" height="84px" loading="lazy">
|
||||
<p>A matrix times its inverse is the matrix equivalent of 1, and because "something times 1" is the same as "something", so we can just outright remove any matrix/inverse pair:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg" width="369px" height="88px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg" width="372px" height="84px" loading="lazy">
|
||||
<p>And now we're <em>basically</em> done. We just multiply those two matrices and we know what <em>V</em> is:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg" width="161px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg" width="160px" height="73px" loading="lazy">
|
||||
<p>We now have the final piece of our function puzzle. Let's run through each step.</p>
|
||||
<ol>
|
||||
<li>Start with the Catmull-Rom function:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="409px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="423px" height="75px" loading="lazy">
|
||||
<ol start="2">
|
||||
<li>rewrite to pure coordinate form:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg" width="324px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg" width="329px" height="79px" loading="lazy">
|
||||
<ol start="3">
|
||||
<li>rewrite for "normal" coordinate vector:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg" width="441px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg" width="448px" height="76px" loading="lazy">
|
||||
<ol start="4">
|
||||
<li>merge the inner matrices:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg" width="348px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg" width="348px" height="76px" loading="lazy">
|
||||
<ol start="5">
|
||||
<li>rewrite for Bézier matrix form:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg" width="431px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg" width="436px" height="75px" loading="lazy">
|
||||
<ol start="6">
|
||||
<li>and transform the coordinates so we have a "pure" Bézier expression:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg" width="348px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg" width="353px" height="77px" loading="lazy">
|
||||
<p>And we're done: we finally know how to convert these two curves!</p>
|
||||
</div>
|
||||
|
||||
<p>If we have a Catmull-Rom curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, then we can draw that curve using a Bézier curve that has the vector:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg" width="249px" height="85px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg" width="268px" height="81px" loading="lazy">
|
||||
<p>Similarly, if we have a Bézier curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, we can draw that using a standard tension Catmull-Rom curve with the following coordinate values:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg" width="284px" height="77px" loading="lazy">
|
||||
<p>or, if your API requires specifying Catmull-Rom curves using "point + tangent" form:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg" width="284px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg" width="300px" height="79px" loading="lazy">
|
||||
<p>Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg" width="300px" height="80px" loading="lazy">
|
||||
|
||||
</section>
|
||||
<section id="catmullmolding">
|
||||
|
@@ -200,13 +200,15 @@
|
||||
<div class="figure">
|
||||
<graphics-element title="2次のベジエ曲線" width="275" height="275" src="./chapters/introduction/quadratic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\54e9ec0600ac436b0e6f0c6b5005cf03.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>2次のベジエ曲線</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="3次のベジエ曲線" width="275" height="275" src="./chapters/introduction/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\8d158a13e9a86969b99c64057644cbc6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>3次のベジエ曲線</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -222,8 +224,9 @@
|
||||
<p>では、実際に見てみましょう。下の図はインタラクティブになっています。上下キーで補間の比率が増減しますので、どうなるか確かめてみましょう。最初に3点があり、それを結んで2本の直線が引かれています。この直線の上でそれぞれ線形補間を行うと、2つの点が得られます。この2点の間でさらに線形補間を行うと、1つの点を得ることができます。そして、あらゆる比率に対して同様に点を求め、それをすべて集めると、このようにベジエ曲線ができるのです。</p>
|
||||
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" height="275" src="./chapters/whatis/interpolation.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\9b3633889c38325c24c19ce18ab94ad6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -248,8 +251,9 @@
|
||||
<p>というわけで、普通の関数では<i>y</i>座標を<i>x</i>座標によって定義しますが、パラメトリック曲線ではそうではなく、座標の値を「制御」変数と結びつけます。<i>t</i>の値を変化させるたびに<strong>2つ</strong>の値が変化するので、これをグラフ上の座標 (<i>x</i>,<i>y</i>)として使うことができます。例えば、先ほどの関数の組は円周上の点を生成します。負の無限大から正の無限大へと<i>t</i>を動かすと、得られる座標(<i>x</i>,<i>y</i>)は常に中心(0,0)・半径1の円の上に乗ります。<i>t</i>を0から5まで変化させてプロットした場合は、このようになります。</p>
|
||||
<graphics-element title="(部分)円 x=sin(t), y=cos(t)" width="275" height="275" src="./chapters/explanation/circle.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\explanation\6d2f915735ebdc05e42c0ea7adc85343.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>(部分)円 x=sin(t), y=cos(t)</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="10" step="0.1" value="5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -330,24 +334,27 @@ function Bezier(3,t):
|
||||
<div class="figure">
|
||||
<graphics-element title="2次の補間" width="275" height="275" src="./chapters/control/lerp.js" data-degree="3">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\ecc15848fbe7b2176b0c89973f07c694.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>2次の補間</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="3次の補間" width="275" height="275" src="./chapters/control/lerp.js" data-degree="4">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\49423783987ac4bc49fbe4c519dbc1d1.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>3次の補間</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="15次の補間" width="275" height="275" src="./chapters/control/lerp.js" data-degree="15">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\afd21a9ba16965c2e7ec2d0d14892250.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>15次の補間</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -402,8 +409,9 @@ function Bezier(3,t,w[]):
|
||||
<p>But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:</p>
|
||||
<graphics-element title="Our rational cubic Bézier curve" width="275" height="275" src="./chapters/weightcontrol/rational.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\weightcontrol\0ef06657f0540938686b9b35b249a22b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Our rational cubic Bézier curve</label>
|
||||
</fallback-image>
|
||||
ratio 1 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
ratio 2 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
@@ -460,13 +468,15 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<div class="figure">
|
||||
<graphics-element title="無限区間の2次ベジエ曲線" width="275" height="275" src="./chapters/extended/extended.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\extended\391a61142c56b79260680aefb08cd9c4.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>無限区間の2次ベジエ曲線</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="無限区間の3次ベジエ曲線" width="275" height="275" src="./chapters/extended/extended.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\extended\baeceec6e1587794b8b275a90d5d85e9.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>無限区間の3次ベジエ曲線</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -517,8 +527,9 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>下の図にマウスを乗せると、この様子を実際に見ることができます。ド・カステリョのアルゴリズムによって曲線上の点を明示的に計算していますが、マウスを動かすと求める点が変わります。マウスカーソルを左から右へ(もちろん、右から左へでも)動かせば、このアルゴリズムによって曲線が生成される様子がわかります。</p>
|
||||
<graphics-element title="ド・カステリョのアルゴリズムで曲線をたどる" width="275" height="275" src="./chapters/decasteljau/decasteljau.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\decasteljau\715d1d2eecc762d6bc1470954b145018.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>ド・カステリョのアルゴリズムで曲線をたどる</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -557,16 +568,18 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<div class="figure">
|
||||
<graphics-element title="2次ベジエ曲線の平坦化" width="275" height="275" src="./chapters/flattening/flatten.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\flattening\3deec756c96e53127cd1d615c61043ae.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>2次ベジエ曲線の平坦化</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="16" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="3次ベジエ曲線の平坦化" width="275" height="275" src="./chapters/flattening/flatten.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\flattening\e2bb7113d5cda2e3fd29bbc54fbe8841.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>3次ベジエ曲線の平坦化</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -601,8 +614,9 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>ベジエ曲線を分割して、繫ぎ合わせたときに元に戻るような小さい2曲線にしたい場合にも、ド・カステリョのアルゴリズムを使えば、これに必要な点をすべて求めることができます。ある値<code>t</code>に対してド・カステリョの骨格を組み立てると、その<code>t</code>で曲線を分割する際に必要になる点がすべて得られます。骨格内部の点のうち、曲線上の点から見て手前側にある点によって一方の曲線が定義され、向こう側にある点によってもう一方の曲線が定義されます。</p>
|
||||
<graphics-element title="曲線の分割" width="825" height="275" src="./chapters/splitting/splitting.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\bef2f09698c0d3d2b7c4c031be17ff69.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -724,8 +738,9 @@ function drawCurve(points[], t):
|
||||
<p>And we're done: we now have an expression that lets us approximate an <code>n+1</code><sup>th</sup> order curve with a lower <code>n</code><sup>th</sup> order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.</p>
|
||||
<graphics-element title="A variable-order Bézier curve" width="275" height="275" src="./chapters/reordering/reorder.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\reordering\387f931043aabd6c467985c568482636.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A variable-order Bézier curve</label>
|
||||
</fallback-image>
|
||||
<button class="raise">raise</button>
|
||||
<button class="lower">lower</button>
|
||||
@@ -799,13 +814,15 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier tangents and normals" width="275" height="275" src="./chapters/pointvectors/quadratic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointvectors\44d454f380ae84dbe4661bd67c406edc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic Bézier tangents and normals</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic Bézier tangents and normals" width="275" height="275" src="./chapters/pointvectors/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointvectors\58719bde12c1cbd535d5d8af1bef9c2b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic Bézier tangents and normals</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -833,8 +850,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/frenet.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\cbadec403b99dec015ab084ee10e1671.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -907,8 +925,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/rotation-minimizing.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\9983328e50c2156c124bb1ea4ad5c05b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -925,14 +944,16 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.</p>
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\components\008604bc4c53bd7e0d97c99a67812ad1.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\components\5214256129e6396e7ac1f1713fa9c88d.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1067,15 +1088,17 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:</p>
|
||||
<graphics-element title="Quadratic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\9850eec01924deae7fda4400ce44270d.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||||
<graphics-element title="Cubic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\890406c7bc96904224f8f14940bf3e56.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1092,13 +1115,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier bounding box" width="275" height="275" src="./chapters/boundingbox/bbox.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\boundingbox\e2c621442e98e2cd20af7efe1cfb041f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic Bézier bounding box</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic Bézier bounding box" width="275" height="275" src="./chapters/boundingbox/bbox.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\boundingbox\f8989a62ebec9d6f123291c146caab5b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic Bézier bounding box</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1120,13 +1145,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\b3ccd45a72c815388aee6515fe37a486.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\31655f24b7dd8b8871687b6610d9ac0e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1138,13 +1165,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="275" height="275" src="./chapters/tightbounds/tightbounds.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\tightbounds\ccc77ae1f57d7dd7ce4d5397fe1b140b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Aligning a quadratic curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="275" height="275" src="./chapters/tightbounds/tightbounds.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\tightbounds\419415bee6ffd7598c035c42de09a94f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Aligning a cubic curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1183,8 +1212,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>Taking that into account, we compute <em>t</em>, we disregard any <em>t</em> value that isn't in the Bézier interval [0,1], and we now know at which <em>t</em> value(s) our curve will inflect.</p>
|
||||
<graphics-element title="Finding cubic Bézier curve inflections" width="275" height="275" src="./chapters/inflections/inflection.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\inflections\9e1ce3975100600d4979370851929b73.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding cubic Bézier curve inflections</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1195,8 +1225,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
<graphics-element title="The canonical curve map" width="400" height="400" src="./chapters/canonical/canonical.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\675217e6df3d75c86503dc2af623e14e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>This is a fairly funky image, so let's see what the various parts of it mean...</p>
|
||||
@@ -1261,8 +1292,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:</p>
|
||||
<graphics-element title="A cubic curve mapped to canonical form" width="800" height="400" src="./chapters/canonical/interactive.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\344346f09b6d5d7e3ddf91084ad50d46.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1272,8 +1304,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\dd28d64458d22f4fe89c98568258efcb.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1299,8 +1332,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So the procedure is fairly straight forward: pick an <code>x</code>, find the associted <code>t</code> value, evaluate our curve <em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this <code>x</code>. Move the slider for the following graphic to see this in action:</p>
|
||||
<graphics-element title="Finding By(t), by finding t for a given x" width="275" height="275" src="./chapters/yforx/yforx.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\efcfe9b48ca4e65eef3d4bf3e4c97bc3.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding By(t), by finding t for a given x</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1321,18 +1355,21 @@ y = curve.get(t).y</code></pre>
|
||||
<div class="figure">
|
||||
<graphics-element title="A function's approximated integral" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="10">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\580c33f599b70de44b17c546098508aa.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A function's approximated integral</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="A better approximation" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="24">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\0d7138b99f5986332a050a8479eefa57.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A better approximation</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="An even better approximation" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="99">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\475547c773a7279dc037c9ced2c8c6dc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>An even better approximation</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1354,8 +1391,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>If we use the Legendre-Gauss values for our <em>C</em> values (thickness for each strip) and <em>t</em> values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?</p>
|
||||
<graphics-element title="Arc length for a Bézier curve" width="275" height="275" src="./chapters/arclength/arclength.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\195f9a3c60f8dfe977c6450d21968f69.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Arc length for a Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1367,16 +1405,18 @@ y = curve.get(t).y</code></pre>
|
||||
|
||||
<graphics-element title="Approximate quadratic curve arc length" width="275" height="275" src="./chapters/arclengthapprox/approximate.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclengthapprox\a040f6b7c7c33ada25ecfd1060726545.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Approximate quadratic curve arc length</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="24" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="Approximate cubic curve arc length" width="275" height="275" src="./chapters/arclengthapprox/approximate.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclengthapprox\c270144cc41e4ebc4b0b2331473530fa.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Approximate cubic curve arc length</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="32" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1422,8 +1462,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.</p>
|
||||
<graphics-element title="Matching curvatures for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\5fcfb0572cae06717506c84768aa568c.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>One thing you may have noticed in this sketch is that sometimes the curvature looks fine, but seems to be pointing in the wrong direction, making it hard to line up the curves properly. A way around that, of course, is to show the curvature on both sides of the curve, so let's just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:</p>
|
||||
@@ -1431,8 +1472,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:</p>
|
||||
<graphics-element title="(Easier) curvature matching for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" data-omni="true">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\876d7b2750d7c29068ac6181c3634d25.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1446,16 +1488,18 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\52f815cefe99dabc47ca83d0b97b61fc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>So, how do we "cut up" the arc length function at regular intervals, when we can't really work with it? We basically cheat: we run through the curve using <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and then we find "the closest <code>t</code> value that matches some required distance" using those values instead. If we have a low number of points sampled, we can then even refine which <code>t</code> value "should" work for our desired distance by interpolating between two points, but if we have a high enough number of samples, we don't even need to bother.</p>
|
||||
<p>So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\133bf9d02801a3149c9ddb8b313e6797.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1473,8 +1517,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).</p>
|
||||
<graphics-element title="Line/line intersections" width="275" height="275" src="./chapters/intersections/line-line.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\61876a2bd727df377619c5ad34ce86be.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Line/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<div class="howtocode">
|
||||
@@ -1505,13 +1550,15 @@ lli = function(line1, line2):
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic curve/line intersections" width="275" height="275" src="./chapters/intersections/curve-line.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\594c2df534a1736c03cd3a96ff4a9913.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic curve/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic curve/line intersections" width="275" height="275" src="./chapters/intersections/curve-line.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\dc26a6063dadc31d242f1c1c8f38bb5e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic curve/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1537,8 +1584,9 @@ lli = function(line1, line2):
|
||||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||||
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\3689ed0c15eace45a1f6ae03909ad8ed.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
<button class="next">Advance one step</button>
|
||||
@@ -1558,15 +1606,17 @@ lli = function(line1, line2):
|
||||
|
||||
<graphics-element inline={true} title="Projections in a quadratic Bézier curve" width="275" height="275" src="./chapters/abc/abc.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\abc\7a69dd4350ddda5701712e1d3b46b863.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Projections in a quadratic Bézier curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
<graphics-element inline={true} title="Projections in a cubic Bézier curve" width="275" height="275" src="./chapters/abc/abc.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\abc\eeec7cf16fb22c666e0143a3a030731f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Projections in a cubic Bézier curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1612,16 +1662,18 @@ lli = function(line1, line2):
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\067a3df30e32708fc0d13f8eb78c0b05.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Fitting a quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\173ea31517a72a927d561f121f0677db.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding a circle through three points</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<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>
|
||||
@@ -1633,15 +1685,17 @@ lli = function(line1, line2):
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\8d045d352f5017b65e60620b92d7ae29.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding the cubic e₁ and e₂ given three points </label>
|
||||
</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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\eab6ea46fa93030e03ec0ef7deb571dc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Fitting a quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>That looks perfectly servicable!</p>
|
||||
@@ -1669,8 +1723,9 @@ for (coordinate, index) in LUT:
|
||||
<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" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1684,24 +1739,27 @@ for (coordinate, index) in LUT:
|
||||
<p>And we're done, because that's our new quadratic control point!</p>
|
||||
<graphics-element title="Molding a quadratic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\6b91671c2962530b863ae0da5789a9cc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>As before, cubic curves are a bit more work, because while it's easy to find our initial <code>t</code> value and ABC values, getting those all-important <code>e1</code> and <code>e2</code> coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate <code>t</code> value ourselves, which allowed us to find appropriate <code>e1</code> and <code>e2</code> 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 <code>t</code> value, and its own <code>e1</code> and <code>e2</code> values, and those may not make sense for the rest of the curve.</p>
|
||||
<p>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 <code>t</code> value and <code>e1</code>/<code>e2</code> coordinates:</p>
|
||||
<graphics-element title="Molding a cubic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\522f1edd37163772b81acb86d3a4f423.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>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.</p>
|
||||
<p>One way to combat this might be to combine the above approach with the approach from the <a href="#pointcurves">creating curves</a> section: generate both the "unchanged <code>t</code>/<code>e1</code>/<code>e2</code>" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised <code>t</code> value, and then interpolating between those two curves:</p>
|
||||
<graphics-element title="Molding a cubic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="cubic" data-interpolated="true">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\ea1656c068a60631135ad499e8a29453.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="200" step="1" value="100" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1785,8 +1843,9 @@ for (coordinate, index) in LUT:
|
||||
<p>So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least three. You can click for more points, and the code will simply try to compute an exact fit using a Bézier curve of the appropriate order. Four points? Cubic Bézier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once there's enough points for compound <a href="https://en.wikipedia.org/wiki/Round-off_error#Floating-point_number_system">floating point rounding errors</a> to start making a difference (which is around 10~11 points).</p>
|
||||
<graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\curvefitting\c2c92587a184efa6c4fee45e4a3e32ed.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\curvefitting\c6c8442e24793ce72a872ce29b2b4125.png">
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<button class="toggle">toggle</button>
|
||||
<div class="sliders"><!-- this will contain range inputs, created by the graphic --></div>
|
||||
@@ -1797,83 +1856,92 @@ for (coordinate, index) in LUT:
|
||||
</section>
|
||||
<section id="catmullconv">
|
||||
<h1><a href="ja-JP/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></h1>
|
||||
<p>Taking an excursion to different splines, the other common design curve is the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>. Now, a Catmull-Rom spline is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as it so happens the cubic Bézier curve is <em>also</em> a cubic Hermite spline, so maybe... maybe we can convert one into the other, and back, with some simple substitutions?</p>
|
||||
<p>Unlike Bézier curves, Catmull-Rom splines pass through each point used to define the curve, except the first and last, which makes sense if you read the "natural language" description for how a Catmull-Rom spline works: a Catmull-Rom spline is a curve that, at each point P<sub>x</sub>, has a tangent along the line P<sub>x-1</sub> to P<sub>x+1</sub>. The curve runs from points P<sub>2</sub> to P<sub>n-1</sub>, and has a "tension" that determines how fast the curve passes through each point. The lower the tension, the faster the curve goes through each point, and the bigger its local tangent is.</p>
|
||||
<p>I'll be showing the conversion to and from Catmull-Rom curves for the tension that the Processing language uses for its Catmull-Rom algorithm.</p>
|
||||
<p>We start with showing the Catmull-Rom matrix form, which looks similar to the Bézier matrix form, with slightly different values in the matrix:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg" width="409px" height="75px" loading="lazy">
|
||||
<p>However, there's something funny going on here: the coordinate column matrix looks weird. The reason is that Catmull-Rom curves are actually curve segments that are described by two coordinate points, and two tangents; the curve starts at coordinate V1, and ends at coordinate V2, with the curve "departing" V1 with a tangent vector V'1 and "arriving" at V2 with tangent vector V'2.</p>
|
||||
<p>This is not particularly useful if we want to draw Catmull-Rom curves in the same way we draw Bézier curves, i.e. by providing four points. However, we can fairly easily go from the former to the latter, but it's going to require some linear algebra, so if you just want to know how to convert between the two coordinate systems: skip the following bit.</p>
|
||||
<p>But... if you want to know <em>why</em> that conversion works, let's do some maths!</p>
|
||||
<p>Taking an excursion to different splines, the other common design curve is the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>, which unlike Bézier curves pass <em>through</em> the points you define. In fact, let's start with just playing with one: the following graphic has a predefined curve that you manipulate the points for, or you can click/tap somewhere to extend the curve.</p>
|
||||
<graphics-element title="A Catmull-Rom curve" width="275" height="275" src="./chapters/catmullconv/catmull-rom.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\catmullconv\b0cb0cccdbea86dcdd610a871e86183f.png">
|
||||
<label>A Catmull-Rom curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0.1" max="1" step="0.01" value="0.5" class="slide-control tension">
|
||||
</graphics-element>
|
||||
|
||||
<p>You may have noticed the slider that seems to control the "tension" of the curve: that's a feature of Catmull-Rom curves; because Catmull-Rom curves pass through points, the curve tightness is controlled by a tension factor, rather than by moving control points around.</p>
|
||||
<p>What you may <em>also</em> have noticed is that the Catmull-Rom curve seems to just go on forever: add as many points as you like, same curve, rigth? Well, sort of. Catmull-Rom curves are splines, a type of curve with arbitrary number of points, technically consisting of "segments between sets of points", but with maths that makes all the segments line up neatly. As such, at its core a Catmull-Rom consists of two points, and draws a curve between them based on the tangents at those points.</p>
|
||||
<p>Now, a Catmull-Rom spline is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as it so happens, the cubic Bézier curve is <em>also</em> a cubic Hermite spline, so in an interesting bit of programming maths: we can losslessly convert between the two, and the maths (well, the final maths) is surprisingly simple!</p>
|
||||
<p>The main difference between Catmull-Rom curves and Bezier curves is "what the points mean". A cubic Bézier curve is defined by a start point, a control point that implies the tangent at the start, a control point that implies the tangent at the end, and an end point. A Catmull-Rom curve is defined by a start point, a tangent that for that starting point, an end point, and a tangent for that end point. Those are <em>very</em> similar, so let's see exactly <em>how</em> similar they are.</p>
|
||||
<p>We start with the matrix form of thee Catmull-Rom curve, which looks similar to the Bézier matrix form, with slightly different values in the matrix, and a slightly different coordinate vector:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg" width="423px" height="75px" loading="lazy">
|
||||
<p>So the question is: how can we convert that expression with Catmull-Rom matrix and vector into an expression that uses Bézier matrix and vector? The short answer is of course "by using linear algebra", but the real answer is a bit longer, and involves some maths that you may not even care for: just skip over the next bit to see the incredibly simple conversions between the formats, but if you want to know <em>why</em>... let's go!</p>
|
||||
<div class="note">
|
||||
|
||||
<h2>Deriving the conversion formulae</h2>
|
||||
<p>In order to convert between Catmull-Rom curves and Bézier curves, we need to know two things. Firstly, how to express the Catmull-Rom curve using a "set of four coordinates", rather than a mix of coordinates and tangents, and secondly, how to convert those Catmull-Rom coordinates to and from Bézier form.</p>
|
||||
<p>So, let's start with the first, where we want to satisfy the following equality:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg" width="187px" height="83px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg" width="196px" height="79px" loading="lazy">
|
||||
<p>This mapping says that in order to map a Catmull-Rom "point + tangent" vector to something based on an "all coordinates" vector, we need to determine the mapping matrix such that applying <em>T</em> yields P2 as start point, P3 as end point, and two tangents based on the lines between P1 and P3, and P2 nd P4, respectively.</p>
|
||||
<p>Computing <em>T</em> is really more "arranging the numbers":</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg" width="591px" height="83px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg" width="621px" height="79px" loading="lazy">
|
||||
<p>Thus:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg" width="143px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg" width="145px" height="76px" loading="lazy">
|
||||
<p>However, we're not <em>quite</em> done, because Catmull-Rom curves have a parameter called "tension", written as τ ("tau"), which is a scaling factor for the tangent vectors: the bigger the tension, the smaller the tangents, and the smaller the tension, the bigger the tangents. As such, the tension factor goes in the denominator for the tangents, and before we continue, let's add that tension factor into both our coordinate vector representation, and mapping matrix <em>T</em>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg" width="285px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg" width="291px" height="79px" loading="lazy">
|
||||
<p>With the mapping matrix properly done, let's rewrite the "point + tangent" Catmull-Rom matrix form to a matrix form in terms of four coordinates, and see what we end up with:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="409px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="423px" height="75px" loading="lazy">
|
||||
<p>Replace point/tangent vector with the expression for all-coordinates:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg" width="549px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg" width="564px" height="76px" loading="lazy">
|
||||
<p>and merge the matrices:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg" width="455px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg" width="465px" height="76px" loading="lazy">
|
||||
<p>This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg" width="353px" height="73px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg" width="359px" height="75px" loading="lazy">
|
||||
<p>So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg" width="227px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg" width="227px" height="76px" loading="lazy">
|
||||
<p>Into something that looks like this:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg" width="167px" height="73px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg" width="171px" height="75px" loading="lazy">
|
||||
<p>And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg" width="440px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg" width="452px" height="76px" loading="lazy">
|
||||
<p>Then we remove the coordinate vector from both sides without affecting the equality:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg" width="353px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg" width="356px" height="76px" loading="lazy">
|
||||
<p>Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg" width="657px" height="88px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg" width="672px" height="84px" loading="lazy">
|
||||
<p>A matrix times its inverse is the matrix equivalent of 1, and because "something times 1" is the same as "something", so we can just outright remove any matrix/inverse pair:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg" width="369px" height="88px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg" width="372px" height="84px" loading="lazy">
|
||||
<p>And now we're <em>basically</em> done. We just multiply those two matrices and we know what <em>V</em> is:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg" width="161px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg" width="160px" height="73px" loading="lazy">
|
||||
<p>We now have the final piece of our function puzzle. Let's run through each step.</p>
|
||||
<ol>
|
||||
<li>Start with the Catmull-Rom function:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="409px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="423px" height="75px" loading="lazy">
|
||||
<ol start="2">
|
||||
<li>rewrite to pure coordinate form:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg" width="324px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg" width="329px" height="79px" loading="lazy">
|
||||
<ol start="3">
|
||||
<li>rewrite for "normal" coordinate vector:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg" width="441px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg" width="448px" height="76px" loading="lazy">
|
||||
<ol start="4">
|
||||
<li>merge the inner matrices:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg" width="348px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg" width="348px" height="76px" loading="lazy">
|
||||
<ol start="5">
|
||||
<li>rewrite for Bézier matrix form:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg" width="431px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg" width="436px" height="75px" loading="lazy">
|
||||
<ol start="6">
|
||||
<li>and transform the coordinates so we have a "pure" Bézier expression:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg" width="348px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg" width="353px" height="77px" loading="lazy">
|
||||
<p>And we're done: we finally know how to convert these two curves!</p>
|
||||
</div>
|
||||
|
||||
<p>If we have a Catmull-Rom curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, then we can draw that curve using a Bézier curve that has the vector:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg" width="249px" height="85px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg" width="268px" height="81px" loading="lazy">
|
||||
<p>Similarly, if we have a Bézier curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, we can draw that using a standard tension Catmull-Rom curve with the following coordinate values:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg" width="284px" height="77px" loading="lazy">
|
||||
<p>or, if your API requires specifying Catmull-Rom curves using "point + tangent" form:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg" width="284px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg" width="300px" height="79px" loading="lazy">
|
||||
<p>Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg" width="300px" height="80px" loading="lazy">
|
||||
|
||||
</section>
|
||||
<section id="catmullmolding">
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { enrich } from "../lib/enrich.js";
|
||||
import { Bezier } from "./types/bezier.js";
|
||||
import { Vector } from "./types/vector.js";
|
||||
import { Matrix } from "./types/matrix.js";
|
||||
import { Shape } from "./util/shape.js";
|
||||
import { Matrix } from "./util/matrix.js";
|
||||
import binomial from "./util/binomial.js";
|
||||
import { BaseAPI } from "./base-api.js";
|
||||
|
||||
const MOUSE_PRECISION_ZONE = 5;
|
||||
@@ -169,12 +170,18 @@ class GraphicsAPI extends BaseAPI {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
slider.value = initial;
|
||||
this[propname] = parseFloat(slider.value);
|
||||
|
||||
let handlerName = `on${propname[0].toUpperCase()}${propname
|
||||
.substring(1)
|
||||
.toLowerCase()}`;
|
||||
|
||||
if (this[handlerName]) {
|
||||
this[handlerName](initial);
|
||||
} else {
|
||||
slider.value = initial;
|
||||
}
|
||||
|
||||
slider.listen(`input`, (evt) => {
|
||||
this[propname] = parseFloat(evt.target.value);
|
||||
if (this[handlerName]) this[handlerName](this[propname]);
|
||||
@@ -725,6 +732,10 @@ class GraphicsAPI extends BaseAPI {
|
||||
return Math.pow(v, p);
|
||||
}
|
||||
|
||||
binomial(n, k) {
|
||||
return binomial(n, k);
|
||||
}
|
||||
|
||||
map(v, s, e, ns, ne, constrain = false) {
|
||||
const i1 = e - s,
|
||||
i2 = ne - ns,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Vector } from "./vector.js";
|
||||
import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
|
||||
import { fitCurveToPoints } from "../util/fit-curve-to-points.js";
|
||||
|
||||
/**
|
||||
* A canvas-aware Bezier curve class
|
||||
@@ -23,6 +24,30 @@ class Bezier extends Original {
|
||||
return new Bezier(apiInstance, 110, 150, 25, 190, 210, 250, 210, 30);
|
||||
}
|
||||
|
||||
static fitCurveToPoints(apiInstance, points, tvalues) {
|
||||
if (!tvalues) {
|
||||
const D = [0];
|
||||
for (let i = 1; i < n; i++) {
|
||||
D[i] =
|
||||
D[i - 1] +
|
||||
dist(points[i - 1].x, points[i - 1].y, points[i].x, points[i].y);
|
||||
}
|
||||
const S = [],
|
||||
len = D[n - 1];
|
||||
D.forEach((v, i) => {
|
||||
S[i] = v / len;
|
||||
});
|
||||
tvalues = S;
|
||||
}
|
||||
|
||||
const bestFitData = fitCurveToPoints(points, tvalues),
|
||||
x = bestFitData.x,
|
||||
y = bestFitData.y,
|
||||
bpoints = x.map((r, i) => ({ x: r[0], y: y[i][0] }));
|
||||
|
||||
return new Bezier(apiInstance, bpoints);
|
||||
}
|
||||
|
||||
constructor(apiInstance, ...coords) {
|
||||
if (!apiInstance || !apiInstance.setMovable) {
|
||||
throw new Error(
|
||||
|
@@ -125,9 +125,33 @@ function transpose(M) {
|
||||
}
|
||||
|
||||
class Matrix {
|
||||
constructor(data) {
|
||||
constructor(n, m, data) {
|
||||
data = n instanceof Array ? n : data;
|
||||
this.data =
|
||||
data ?? [...new Array(n)].map((v) => [...new Array(m)].map((v) => 0));
|
||||
this.rows = this.data.length;
|
||||
this.cols = this.data[0].length;
|
||||
}
|
||||
setData(data) {
|
||||
this.data = data;
|
||||
}
|
||||
get(i, j) {
|
||||
return this.data[i][j];
|
||||
}
|
||||
set(i, j, value) {
|
||||
this.data[i][j] = value;
|
||||
}
|
||||
row(i) {
|
||||
return this.data[i];
|
||||
}
|
||||
col(i) {
|
||||
var d = this.data,
|
||||
col = [];
|
||||
for (let r = 0, l = d.length; r < l; r++) {
|
||||
col.push(d[r][i]);
|
||||
}
|
||||
return col;
|
||||
}
|
||||
multiply(other) {
|
||||
return new Matrix(multiply(this.data, other.data));
|
||||
}
|
21
docs/js/custom-element/api/util/binomial.js
Normal file
@@ -0,0 +1,21 @@
|
||||
var binomialCoefficients = [[1], [1, 1]];
|
||||
|
||||
/**
|
||||
* ... docs go here ...
|
||||
*/
|
||||
function binomial(n, k) {
|
||||
if (n === 0) return 1;
|
||||
var lut = binomialCoefficients;
|
||||
while (n >= lut.length) {
|
||||
var s = lut.length;
|
||||
var nextRow = [1];
|
||||
for (var i = 1, prev = s - 1; i < s; i++) {
|
||||
nextRow[i] = lut[prev][i - 1] + lut[prev][i];
|
||||
}
|
||||
nextRow[s] = 1;
|
||||
lut.push(nextRow);
|
||||
}
|
||||
return lut[n][k];
|
||||
}
|
||||
|
||||
export default binomial;
|
86
docs/js/custom-element/api/util/fit-curve-to-points.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Matrix } from "../types/matrix.js";
|
||||
import binomial from "./binomial.js";
|
||||
|
||||
/*
|
||||
We can form any basis matrix using a generative approach:
|
||||
|
||||
- it's an M = (n x n) matrix
|
||||
- it's a lower triangular matrix: all the entries above the main diagonal are zero
|
||||
- the main diagonal consists of the binomial coefficients for n
|
||||
- all entries are symmetric about the antidiagonal.
|
||||
|
||||
What's more, if we number rows and columns starting at 0, then
|
||||
the value at position M[r,c], with row=r and column=c, can be
|
||||
expressed as:
|
||||
|
||||
M[r,c] = (r choose c) * M[r,r] * S,
|
||||
|
||||
where S = 1 if r+c is even, or -1 otherwise
|
||||
|
||||
That is: the values in column c are directly computed off of the
|
||||
binomial coefficients on the main diagonal, through multiplication
|
||||
by a binomial based on matrix position, with the sign of the value
|
||||
also determined by matrix position. This is actually very easy to
|
||||
write out in code:
|
||||
*/
|
||||
function generateBasisMatrix(n) {
|
||||
const M = new Matrix(n, n);
|
||||
|
||||
// populate the main diagonal
|
||||
var k = n - 1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
M.set(i, i, binomial(k, i));
|
||||
}
|
||||
|
||||
// compute the remaining values
|
||||
for (var c = 0, r; c < n; c++) {
|
||||
for (r = c + 1; r < n; r++) {
|
||||
var sign = (r + c) % 2 === 0 ? 1 : -1;
|
||||
var value = binomial(r, c) * M.get(r, r);
|
||||
M.set(r, c, sign * value);
|
||||
}
|
||||
}
|
||||
|
||||
return M;
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
function formTMatrix(row, n) {
|
||||
let data = [];
|
||||
for (var i = 0; i < n; i++) {
|
||||
data.push(row.map((v) => v ** i));
|
||||
}
|
||||
const Tt = new Matrix(n, n, data);
|
||||
const T = Tt.transpose();
|
||||
return { T, Tt };
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
function computeBestFit(points, n, M, S) {
|
||||
var tm = formTMatrix(S, n),
|
||||
T = tm.T,
|
||||
Tt = tm.Tt,
|
||||
M1 = M.invert(),
|
||||
TtT1 = Tt.multiply(T).invert(),
|
||||
step1 = TtT1.multiply(Tt),
|
||||
step2 = M1.multiply(step1),
|
||||
X = new Matrix(points.map((v) => [v.x])),
|
||||
Cx = step2.multiply(X),
|
||||
Y = new Matrix(points.map((v) => [v.y])),
|
||||
Cy = step2.multiply(Y);
|
||||
return { x: Cx.data, y: Cy.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
function fitCurveToPoints(points, tvalues) {
|
||||
const n = points.length;
|
||||
return computeBestFit(points, n, generateBasisMatrix(n), tvalues);
|
||||
}
|
||||
|
||||
export { fitCurveToPoints };
|
@@ -35,10 +35,8 @@ graphics-element:not(:defined) > * {
|
||||
|
||||
graphics-element:not(:defined) fallback-image {
|
||||
display: block;
|
||||
font-size: 60%;
|
||||
text-align: center;
|
||||
padding-bottom: 0.2em;
|
||||
visibility:collapse;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -46,10 +44,25 @@ graphics-element:not(:defined) fallback-image {
|
||||
treated as a full block, so that the caption text shows up underneath
|
||||
it, rather than next to it:
|
||||
*/
|
||||
graphics-element:not(:defined) fallback-image > .view-source {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -0.6em;
|
||||
margin-bottom: -0.2em;
|
||||
font-size: 60%;
|
||||
color: #00000030;
|
||||
}
|
||||
|
||||
graphics-element:not(:defined) fallback-image > label {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
graphics-element:not(:defined) fallback-image > img {
|
||||
display: block;
|
||||
visibility:visible;
|
||||
border: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -6,6 +6,20 @@ import performCodeSurgery from "./lib/perform-code-surgery.js";
|
||||
const MODULE_URL = import.meta.url;
|
||||
const MODULE_PATH = MODULE_URL.slice(0, MODULE_URL.lastIndexOf(`/`));
|
||||
|
||||
// Really wish this was baked into the DOM API...
|
||||
function isInViewport(e) {
|
||||
if (typeof window === `undefined`) return true;
|
||||
if (typeof document === `undefined`) return true;
|
||||
|
||||
var b = e.getBoundingClientRect();
|
||||
return (
|
||||
b.top >= 0 &&
|
||||
b.left >= 0 &&
|
||||
b.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
b.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple "for programming code" element, for holding entire
|
||||
* programs, rather than code snippets.
|
||||
@@ -18,21 +32,35 @@ CustomElement.register(class ProgramCode extends HTMLElement {});
|
||||
class GraphicsElement extends CustomElement {
|
||||
static DEBUG = false;
|
||||
|
||||
/**
|
||||
* Create an instance of this element
|
||||
*/
|
||||
constructor() {
|
||||
super({ header: false, footer: false });
|
||||
|
||||
// Strip out fallback images: if we can get here,
|
||||
// we should not be loading fallback images because
|
||||
// we know we're showing live content instead.
|
||||
let fallback = this.querySelector(`fallback-image`);
|
||||
if (fallback) this.removeChild(fallback);
|
||||
|
||||
this.loadSource();
|
||||
|
||||
if (this.title) {
|
||||
this.label = document.createElement(`label`);
|
||||
this.label.textContent = this.title;
|
||||
// Do we load immediately?
|
||||
if (isInViewport(this)) {
|
||||
this.loadSource();
|
||||
}
|
||||
|
||||
// Or do we load later, once we've been scrolled into view?
|
||||
else {
|
||||
let fallback = this.querySelector(`img`);
|
||||
new IntersectionObserver(
|
||||
(entries, observer) =>
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadSource();
|
||||
observer.disconnect();
|
||||
}
|
||||
}),
|
||||
{ threshold: 0.1, rootMargin: `${window.innerHeight}px` }
|
||||
).observe(fallback);
|
||||
}
|
||||
|
||||
this.label = document.createElement(`label`);
|
||||
if (!this.title) this.title = ``;
|
||||
this.label.textContent = this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +81,7 @@ class GraphicsElement extends CustomElement {
|
||||
* part of the CustomElement API
|
||||
*/
|
||||
handleChildChanges(added, removed) {
|
||||
// console.log(`child change:`, added, removed);
|
||||
// debugLog(`child change:`, added, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +108,8 @@ class GraphicsElement extends CustomElement {
|
||||
* Load the graphics code, either from a src URL, a <program-code> element, or .textContent
|
||||
*/
|
||||
async loadSource() {
|
||||
debugLog(`loading ${this.getAttribute(`src`)}`);
|
||||
|
||||
let src = false;
|
||||
let codeElement = this.querySelector(`program-code`);
|
||||
|
||||
@@ -189,6 +219,7 @@ class GraphicsElement extends CustomElement {
|
||||
script.src = `data:application/javascript;charset=utf-8,${encodeURIComponent(
|
||||
this.code
|
||||
)}`;
|
||||
|
||||
if (rerender) this.render();
|
||||
}
|
||||
|
||||
@@ -198,6 +229,9 @@ class GraphicsElement extends CustomElement {
|
||||
setGraphic(apiInstance) {
|
||||
this.apiInstance = apiInstance;
|
||||
this.setCanvas(apiInstance.canvas);
|
||||
// at this point we can remove our placeholder image for this element, too.
|
||||
let fallback = this.querySelector(`fallback-image`);
|
||||
if (fallback) this.removeChild(fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,14 +256,12 @@ class GraphicsElement extends CustomElement {
|
||||
* can't actually find anywhere in the document or shadow DOM...
|
||||
*/
|
||||
printCodeDueToError() {
|
||||
if (GraphicsElement.DEBUG) {
|
||||
console.log(
|
||||
this.code
|
||||
.split(`\n`)
|
||||
.map((l, pos) => `${pos + 1}: ${l}`)
|
||||
.join(`\n`)
|
||||
);
|
||||
}
|
||||
debugLog(
|
||||
this.code
|
||||
.split(`\n`)
|
||||
.map((l, pos) => `${pos + 1}: ${l}`)
|
||||
.join(`\n`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,4 +316,11 @@ if (typeof window !== undefined) {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// debugging should be behind a flag
|
||||
function debugLog(...data) {
|
||||
if (GraphicsElement.DEBUG) {
|
||||
console.log(...data);
|
||||
}
|
||||
}
|
||||
|
||||
export { GraphicsElement };
|
||||
|
@@ -194,13 +194,15 @@
|
||||
<div class="figure">
|
||||
<graphics-element title="二次贝塞尔曲线" width="275" height="275" src="./chapters/introduction/quadratic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\54e9ec0600ac436b0e6f0c6b5005cf03.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>二次贝塞尔曲线</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="三次贝塞尔曲线" width="275" height="275" src="./chapters/introduction/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\introduction\8d158a13e9a86969b99c64057644cbc6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>三次贝塞尔曲线</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -217,8 +219,9 @@
|
||||
:
|
||||
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" height="275" src="./chapters/whatis/interpolation.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\whatis\9b3633889c38325c24c19ce18ab94ad6.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
|
||||
</graphics-element></p>
|
||||
@@ -242,8 +245,9 @@
|
||||
<p>所以,参数曲线不像一般函数那样,通过<i>x</i>坐标来定义<i>y</i>坐标,而是用一个“控制”变量将它们连接起来。如果改变<i>t</i>的值,每次变化时我们都能得到<strong>两个</strong>值,这可以作为图形中的(<i>x</i>,<i>y</i>)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使<i>t</i>在正负极值间变化,得到的输出(<i>x</i>,<i>y</i>)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出<i>t</i>从0到5时的值,将得到如下图像:</p>
|
||||
<graphics-element title="(一部分的)圆: x=sin(t), y=cos(t)" width="275" height="275" src="./chapters/explanation/circle.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\explanation\6d2f915735ebdc05e42c0ea7adc85343.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>(一部分的)圆: x=sin(t), y=cos(t)</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="10" step="0.1" value="5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -324,24 +328,27 @@ function Bezier(3,t):
|
||||
<div class="figure">
|
||||
<graphics-element title="二次插值" width="275" height="275" src="./chapters/control/lerp.js" data-degree="3">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\ecc15848fbe7b2176b0c89973f07c694.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>二次插值</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="三次插值" width="275" height="275" src="./chapters/control/lerp.js" data-degree="4">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\49423783987ac4bc49fbe4c519dbc1d1.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>三次插值</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="15次插值" width="275" height="275" src="./chapters/control/lerp.js" data-degree="15">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\control\afd21a9ba16965c2e7ec2d0d14892250.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>15次插值</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -396,8 +403,9 @@ function Bezier(3,t,w[]):
|
||||
<p>But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects what gets drawn:</p>
|
||||
<graphics-element title="Our rational cubic Bézier curve" width="275" height="275" src="./chapters/weightcontrol/rational.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\weightcontrol\0ef06657f0540938686b9b35b249a22b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Our rational cubic Bézier curve</label>
|
||||
</fallback-image>
|
||||
ratio 1 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
ratio 2 <input type="range" min="0.01" max="2" value="1" step="0.01"><span>1.0</span><br>
|
||||
@@ -454,13 +462,15 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<div class="figure">
|
||||
<graphics-element title="二次无限区间贝塞尔曲线" width="275" height="275" src="./chapters/extended/extended.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\extended\391a61142c56b79260680aefb08cd9c4.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>二次无限区间贝塞尔曲线</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="三次无限区间贝塞尔曲线" width="275" height="275" src="./chapters/extended/extended.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\extended\baeceec6e1587794b8b275a90d5d85e9.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>三次无限区间贝塞尔曲线</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -511,8 +521,9 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>我们通过实际操作来观察这个过程。在以下的图表中,移动鼠标来改变用de Casteljau算法计算得到的曲线点,左右移动鼠标,可以实时看到曲线是如何生成的。</p>
|
||||
<graphics-element title="用de Casteljau算法来遍历曲线" width="275" height="275" src="./chapters/decasteljau/decasteljau.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\decasteljau\715d1d2eecc762d6bc1470954b145018.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>用de Casteljau算法来遍历曲线</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -551,16 +562,18 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<div class="figure">
|
||||
<graphics-element title="拉平一条二次曲线" width="275" height="275" src="./chapters/flattening/flatten.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\flattening\3deec756c96e53127cd1d615c61043ae.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>拉平一条二次曲线</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="16" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="拉平一条三次曲线" width="275" height="275" src="./chapters/flattening/flatten.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\flattening\e2bb7113d5cda2e3fd29bbc54fbe8841.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>拉平一条三次曲线</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -595,8 +608,9 @@ function RationalBezier(3,t,w[],r[]):
|
||||
<p>使用 de Casteljau 算法我们也可以将一条贝塞尔曲线分割成两条更小的曲线,二者拼接起来即可形成原来的曲线。当采用某个 <code>t</code> 值构造 de Casteljau 算法时,该过程会给到我们在 <code>t</code> 点分割曲线的所有点: 一条曲线包含该曲线上点之前的所有点,另一条曲线包含该曲线上点之后的所有点。</p>
|
||||
<graphics-element title="分割一条曲线" width="825" height="275" src="./chapters/splitting/splitting.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\splitting\bef2f09698c0d3d2b7c4c031be17ff69.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -718,8 +732,9 @@ function drawCurve(points[], t):
|
||||
<p>And we're done: we now have an expression that lets us approximate an <code>n+1</code><sup>th</sup> order curve with a lower <code>n</code><sup>th</sup> order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.</p>
|
||||
<graphics-element title="A variable-order Bézier curve" width="275" height="275" src="./chapters/reordering/reorder.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\reordering\387f931043aabd6c467985c568482636.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A variable-order Bézier curve</label>
|
||||
</fallback-image>
|
||||
<button class="raise">raise</button>
|
||||
<button class="lower">lower</button>
|
||||
@@ -793,13 +808,15 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier tangents and normals" width="275" height="275" src="./chapters/pointvectors/quadratic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointvectors\44d454f380ae84dbe4661bd67c406edc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic Bézier tangents and normals</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic Bézier tangents and normals" width="275" height="275" src="./chapters/pointvectors/cubic.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointvectors\58719bde12c1cbd535d5d8af1bef9c2b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic Bézier tangents and normals</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -827,8 +844,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/frenet.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\cbadec403b99dec015ab084ee10e1671.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -901,8 +919,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:</p>
|
||||
<graphics-element title="Some known and unknown vectors" width="350" height="300" src="./chapters/pointvectors3d/rotation-minimizing.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="350px" height="300px" src="images\chapters\pointvectors3d\9983328e50c2156c124bb1ea4ad5c05b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -919,14 +938,16 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
<p>If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.</p>
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\components\008604bc4c53bd7e0d97c99a67812ad1.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" height="275" src="./chapters/components/components.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\components\5214256129e6396e7ac1f1713fa9c88d.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1061,15 +1082,17 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:</p>
|
||||
<graphics-element title="Quadratic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\9850eec01924deae7fda4400ce44270d.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>And for cubic curves, that means first and second derivatives, in red and purple respectively:</p>
|
||||
<graphics-element title="Cubic Bézier curve extremities" width="825" height="275" src="./chapters/extremities/extremities.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\extremities\890406c7bc96904224f8f14940bf3e56.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1086,13 +1109,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic Bézier bounding box" width="275" height="275" src="./chapters/boundingbox/bbox.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\boundingbox\e2c621442e98e2cd20af7efe1cfb041f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic Bézier bounding box</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic Bézier bounding box" width="275" height="275" src="./chapters/boundingbox/bbox.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\boundingbox\f8989a62ebec9d6f123291c146caab5b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic Bézier bounding box</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1114,13 +1139,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\b3ccd45a72c815388aee6515fe37a486.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="550" height="275" src="./chapters/aligning/aligning.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\aligning\31655f24b7dd8b8871687b6610d9ac0e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1132,13 +1159,15 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="275" height="275" src="./chapters/tightbounds/tightbounds.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\tightbounds\ccc77ae1f57d7dd7ce4d5397fe1b140b.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Aligning a quadratic curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Aligning a cubic curve" width="275" height="275" src="./chapters/tightbounds/tightbounds.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\tightbounds\419415bee6ffd7598c035c42de09a94f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Aligning a cubic curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1177,8 +1206,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>Taking that into account, we compute <em>t</em>, we disregard any <em>t</em> value that isn't in the Bézier interval [0,1], and we now know at which <em>t</em> value(s) our curve will inflect.</p>
|
||||
<graphics-element title="Finding cubic Bézier curve inflections" width="275" height="275" src="./chapters/inflections/inflection.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\inflections\9e1ce3975100600d4979370851929b73.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding cubic Bézier curve inflections</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1189,8 +1219,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
|
||||
<graphics-element title="The canonical curve map" width="400" height="400" src="./chapters/canonical/canonical.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="400px" height="400px" src="images\chapters\canonical\675217e6df3d75c86503dc2af623e14e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>This is a fairly funky image, so let's see what the various parts of it mean...</p>
|
||||
@@ -1255,8 +1286,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:</p>
|
||||
<graphics-element title="A cubic curve mapped to canonical form" width="800" height="400" src="./chapters/canonical/interactive.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="800px" height="400px" src="images\chapters\canonical\344346f09b6d5d7e3ddf91084ad50d46.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1266,8 +1298,9 @@ function getCubicRoots(pa, pb, pc, pd) {
|
||||
<p>We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the <code>x</code> coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.</p>
|
||||
<graphics-element title="Finding t, given x=x(t). Left: our curve, right: the x=x(t) function" width="550" height="275" src="./chapters/yforx/basics.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\yforx\dd28d64458d22f4fe89c98568258efcb.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1293,8 +1326,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So the procedure is fairly straight forward: pick an <code>x</code>, find the associted <code>t</code> value, evaluate our curve <em>for</em> that <code>t</code> value, which gives us the curve's {x,y} coordinate, which means we know <code>y</code> for this <code>x</code>. Move the slider for the following graphic to see this in action:</p>
|
||||
<graphics-element title="Finding By(t), by finding t for a given x" width="275" height="275" src="./chapters/yforx/yforx.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\yforx\efcfe9b48ca4e65eef3d4bf3e4c97bc3.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding By(t), by finding t for a given x</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1315,18 +1349,21 @@ y = curve.get(t).y</code></pre>
|
||||
<div class="figure">
|
||||
<graphics-element title="A function's approximated integral" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="10">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\580c33f599b70de44b17c546098508aa.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A function's approximated integral</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="A better approximation" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="24">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\0d7138b99f5986332a050a8479eefa57.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>A better approximation</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="An even better approximation" width="275" height="275" src="./chapters/arclength/draw-slices.js" data-steps="99">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\475547c773a7279dc037c9ced2c8c6dc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>An even better approximation</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1348,8 +1385,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>If we use the Legendre-Gauss values for our <em>C</em> values (thickness for each strip) and <em>t</em> values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?</p>
|
||||
<graphics-element title="Arc length for a Bézier curve" width="275" height="275" src="./chapters/arclength/arclength.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclength\195f9a3c60f8dfe977c6450d21968f69.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Arc length for a Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1361,16 +1399,18 @@ y = curve.get(t).y</code></pre>
|
||||
|
||||
<graphics-element title="Approximate quadratic curve arc length" width="275" height="275" src="./chapters/arclengthapprox/approximate.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclengthapprox\a040f6b7c7c33ada25ecfd1060726545.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Approximate quadratic curve arc length</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="24" step="1" value="4" class="slide-control">
|
||||
</graphics-element>
|
||||
|
||||
<graphics-element title="Approximate cubic curve arc length" width="275" height="275" src="./chapters/arclengthapprox/approximate.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\arclengthapprox\c270144cc41e4ebc4b0b2331473530fa.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Approximate cubic curve arc length</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="1" max="32" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1416,8 +1456,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being derived based on maths that "ignores" specific function derivative, and instead gives a formulat that smooths out any differences) is exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both curves, giving us the smoothest transition.</p>
|
||||
<graphics-element title="Matching curvatures for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\5fcfb0572cae06717506c84768aa568c.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>One thing you may have noticed in this sketch is that sometimes the curvature looks fine, but seems to be pointing in the wrong direction, making it hard to line up the curves properly. A way around that, of course, is to show the curvature on both sides of the curve, so let's just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer:</p>
|
||||
@@ -1425,8 +1466,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" our curve at some point that we can control by using a slider:</p>
|
||||
<graphics-element title="(Easier) curvature matching for a quadratic and cubic Bézier curve" width="825" height="275" src="./chapters/curvature/curvature.js" data-omni="true">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curvature\876d7b2750d7c29068ac6181c3634d25.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="2" step="0.0005" value="0" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1440,16 +1482,18 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.</p>
|
||||
<graphics-element title="The t-for-distance function" width="550" height="275" src="./chapters/tracing/distance-function.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\tracing\52f815cefe99dabc47ca83d0b97b61fc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>So, how do we "cut up" the arc length function at regular intervals, when we can't really work with it? We basically cheat: we run through the curve using <code>t</code> values, determine the distance-for-this-<code>t</code>-value at each point we generate during the run, and then we find "the closest <code>t</code> value that matches some required distance" using those values instead. If we have a low number of points sampled, we can then even refine which <code>t</code> value "should" work for our desired distance by interpolating between two points, but if we have a high enough number of samples, we don't even need to bother.</p>
|
||||
<p>So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those <code>t</code> values on the real curve looks like, by coloring each section of curve between two distance markers differently:</p>
|
||||
<graphics-element title="Fixed-interval coloring a curve" width="825" height="275" src="./chapters/tracing/tracing.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\tracing\133bf9d02801a3149c9ddb8b313e6797.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="2" max="24" step="1" value="8" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1467,8 +1511,9 @@ y = curve.get(t).y</code></pre>
|
||||
<p>The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).</p>
|
||||
<graphics-element title="Line/line intersections" width="275" height="275" src="./chapters/intersections/line-line.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\61876a2bd727df377619c5ad34ce86be.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Line/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<div class="howtocode">
|
||||
@@ -1499,13 +1544,15 @@ lli = function(line1, line2):
|
||||
<div class="figure">
|
||||
<graphics-element title="Quadratic curve/line intersections" width="275" height="275" src="./chapters/intersections/curve-line.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\594c2df534a1736c03cd3a96ff4a9913.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Quadratic curve/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
<graphics-element title="Cubic curve/line intersections" width="275" height="275" src="./chapters/intersections/curve-line.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\intersections\dc26a6063dadc31d242f1c1c8f38bb5e.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Cubic curve/line intersections</label>
|
||||
</fallback-image></graphics-element>
|
||||
</div>
|
||||
|
||||
@@ -1531,8 +1578,9 @@ lli = function(line1, line2):
|
||||
<p>(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)</p>
|
||||
<graphics-element title="Curve/curve intersections" width="825" height="275" src="./chapters/curveintersection/curve-curve.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\curveintersection\3689ed0c15eace45a1f6ae03909ad8ed.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0.01" max="1" step="0.01" value="1" class="slide-control">
|
||||
<button class="next">Advance one step</button>
|
||||
@@ -1552,15 +1600,17 @@ lli = function(line1, line2):
|
||||
|
||||
<graphics-element inline={true} title="Projections in a quadratic Bézier curve" width="275" height="275" src="./chapters/abc/abc.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\abc\7a69dd4350ddda5701712e1d3b46b863.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Projections in a quadratic Bézier curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
<graphics-element inline={true} title="Projections in a cubic Bézier curve" width="275" height="275" src="./chapters/abc/abc.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\abc\eeec7cf16fb22c666e0143a3a030731f.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Projections in a cubic Bézier curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0.5" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1606,16 +1656,18 @@ lli = function(line1, line2):
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\067a3df30e32708fc0d13f8eb78c0b05.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Fitting a quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\173ea31517a72a927d561f121f0677db.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding a circle through three points</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<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>
|
||||
@@ -1627,15 +1679,17 @@ lli = function(line1, line2):
|
||||
<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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\8d045d352f5017b65e60620b92d7ae29.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Finding the cubic e₁ and e₂ given three points </label>
|
||||
</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>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\pointcurves\eab6ea46fa93030e03ec0ef7deb571dc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label>Fitting a quadratic Bézier curve</label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>That looks perfectly servicable!</p>
|
||||
@@ -1663,8 +1717,9 @@ for (coordinate, index) in LUT:
|
||||
<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" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="320px" height="320px" src="images\chapters\projections\c40ab9e3f3d1f53872dff30a7bcdb003.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
</section>
|
||||
@@ -1678,24 +1733,27 @@ for (coordinate, index) in LUT:
|
||||
<p>And we're done, because that's our new quadratic control point!</p>
|
||||
<graphics-element title="Molding a quadratic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="quadratic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\6b91671c2962530b863ae0da5789a9cc.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>As before, cubic curves are a bit more work, because while it's easy to find our initial <code>t</code> value and ABC values, getting those all-important <code>e1</code> and <code>e2</code> coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate <code>t</code> value ourselves, which allowed us to find appropriate <code>e1</code> and <code>e2</code> 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 <code>t</code> value, and its own <code>e1</code> and <code>e2</code> values, and those may not make sense for the rest of the curve.</p>
|
||||
<p>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 <code>t</code> value and <code>e1</code>/<code>e2</code> coordinates:</p>
|
||||
<graphics-element title="Molding a cubic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="cubic">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\522f1edd37163772b81acb86d3a4f423.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image></graphics-element>
|
||||
|
||||
<p>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.</p>
|
||||
<p>One way to combat this might be to combine the above approach with the approach from the <a href="#pointcurves">creating curves</a> section: generate both the "unchanged <code>t</code>/<code>e1</code>/<code>e2</code>" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised <code>t</code> value, and then interpolating between those two curves:</p>
|
||||
<graphics-element title="Molding a cubic Bézier curve" width="825" height="275" src="./chapters/molding/molding.js" data-type="cubic" data-interpolated="true">
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="825px" height="275px" src="images\chapters\molding\ea1656c068a60631135ad499e8a29453.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<input type="range" min="10" max="200" step="1" value="100" class="slide-control">
|
||||
</graphics-element>
|
||||
@@ -1779,8 +1837,9 @@ for (coordinate, index) in LUT:
|
||||
<p>So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least three. You can click for more points, and the code will simply try to compute an exact fit using a Bézier curve of the appropriate order. Four points? Cubic Bézier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once there's enough points for compound <a href="https://en.wikipedia.org/wiki/Round-off_error#Floating-point_number_system">floating point rounding errors</a> to start making a difference (which is around 10~11 points).</p>
|
||||
<graphics-element title="Fitting a Bézier curve" width="550" height="275" src="./chapters/curvefitting/curve-fitting.js" >
|
||||
<fallback-image>
|
||||
<img width="550px" height="275px" src="images\chapters\curvefitting\c2c92587a184efa6c4fee45e4a3e32ed.png">
|
||||
Scripts are disabled. Showing fallback image.
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="550px" height="275px" src="images\chapters\curvefitting\c6c8442e24793ce72a872ce29b2b4125.png">
|
||||
<label></label>
|
||||
</fallback-image>
|
||||
<button class="toggle">toggle</button>
|
||||
<div class="sliders"><!-- this will contain range inputs, created by the graphic --></div>
|
||||
@@ -1791,83 +1850,92 @@ for (coordinate, index) in LUT:
|
||||
</section>
|
||||
<section id="catmullconv">
|
||||
<h1><a href="zh-CN/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></h1>
|
||||
<p>Taking an excursion to different splines, the other common design curve is the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>. Now, a Catmull-Rom spline is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as it so happens the cubic Bézier curve is <em>also</em> a cubic Hermite spline, so maybe... maybe we can convert one into the other, and back, with some simple substitutions?</p>
|
||||
<p>Unlike Bézier curves, Catmull-Rom splines pass through each point used to define the curve, except the first and last, which makes sense if you read the "natural language" description for how a Catmull-Rom spline works: a Catmull-Rom spline is a curve that, at each point P<sub>x</sub>, has a tangent along the line P<sub>x-1</sub> to P<sub>x+1</sub>. The curve runs from points P<sub>2</sub> to P<sub>n-1</sub>, and has a "tension" that determines how fast the curve passes through each point. The lower the tension, the faster the curve goes through each point, and the bigger its local tangent is.</p>
|
||||
<p>I'll be showing the conversion to and from Catmull-Rom curves for the tension that the Processing language uses for its Catmull-Rom algorithm.</p>
|
||||
<p>We start with showing the Catmull-Rom matrix form, which looks similar to the Bézier matrix form, with slightly different values in the matrix:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg" width="409px" height="75px" loading="lazy">
|
||||
<p>However, there's something funny going on here: the coordinate column matrix looks weird. The reason is that Catmull-Rom curves are actually curve segments that are described by two coordinate points, and two tangents; the curve starts at coordinate V1, and ends at coordinate V2, with the curve "departing" V1 with a tangent vector V'1 and "arriving" at V2 with tangent vector V'2.</p>
|
||||
<p>This is not particularly useful if we want to draw Catmull-Rom curves in the same way we draw Bézier curves, i.e. by providing four points. However, we can fairly easily go from the former to the latter, but it's going to require some linear algebra, so if you just want to know how to convert between the two coordinate systems: skip the following bit.</p>
|
||||
<p>But... if you want to know <em>why</em> that conversion works, let's do some maths!</p>
|
||||
<p>Taking an excursion to different splines, the other common design curve is the <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom spline</a>, which unlike Bézier curves pass <em>through</em> the points you define. In fact, let's start with just playing with one: the following graphic has a predefined curve that you manipulate the points for, or you can click/tap somewhere to extend the curve.</p>
|
||||
<graphics-element title="A Catmull-Rom curve" width="275" height="275" src="./chapters/catmullconv/catmull-rom.js" >
|
||||
<fallback-image>
|
||||
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
|
||||
<img width="275px" height="275px" src="images\chapters\catmullconv\b0cb0cccdbea86dcdd610a871e86183f.png">
|
||||
<label>A Catmull-Rom curve</label>
|
||||
</fallback-image>
|
||||
<input type="range" min="0.1" max="1" step="0.01" value="0.5" class="slide-control tension">
|
||||
</graphics-element>
|
||||
|
||||
<p>You may have noticed the slider that seems to control the "tension" of the curve: that's a feature of Catmull-Rom curves; because Catmull-Rom curves pass through points, the curve tightness is controlled by a tension factor, rather than by moving control points around.</p>
|
||||
<p>What you may <em>also</em> have noticed is that the Catmull-Rom curve seems to just go on forever: add as many points as you like, same curve, rigth? Well, sort of. Catmull-Rom curves are splines, a type of curve with arbitrary number of points, technically consisting of "segments between sets of points", but with maths that makes all the segments line up neatly. As such, at its core a Catmull-Rom consists of two points, and draws a curve between them based on the tangents at those points.</p>
|
||||
<p>Now, a Catmull-Rom spline is a form of <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline">cubic Hermite spline</a>, and as it so happens, the cubic Bézier curve is <em>also</em> a cubic Hermite spline, so in an interesting bit of programming maths: we can losslessly convert between the two, and the maths (well, the final maths) is surprisingly simple!</p>
|
||||
<p>The main difference between Catmull-Rom curves and Bezier curves is "what the points mean". A cubic Bézier curve is defined by a start point, a control point that implies the tangent at the start, a control point that implies the tangent at the end, and an end point. A Catmull-Rom curve is defined by a start point, a tangent that for that starting point, an end point, and a tangent for that end point. Those are <em>very</em> similar, so let's see exactly <em>how</em> similar they are.</p>
|
||||
<p>We start with the matrix form of thee Catmull-Rom curve, which looks similar to the Bézier matrix form, with slightly different values in the matrix, and a slightly different coordinate vector:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg" width="423px" height="75px" loading="lazy">
|
||||
<p>So the question is: how can we convert that expression with Catmull-Rom matrix and vector into an expression that uses Bézier matrix and vector? The short answer is of course "by using linear algebra", but the real answer is a bit longer, and involves some maths that you may not even care for: just skip over the next bit to see the incredibly simple conversions between the formats, but if you want to know <em>why</em>... let's go!</p>
|
||||
<div class="note">
|
||||
|
||||
<h2>Deriving the conversion formulae</h2>
|
||||
<p>In order to convert between Catmull-Rom curves and Bézier curves, we need to know two things. Firstly, how to express the Catmull-Rom curve using a "set of four coordinates", rather than a mix of coordinates and tangents, and secondly, how to convert those Catmull-Rom coordinates to and from Bézier form.</p>
|
||||
<p>So, let's start with the first, where we want to satisfy the following equality:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg" width="187px" height="83px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg" width="196px" height="79px" loading="lazy">
|
||||
<p>This mapping says that in order to map a Catmull-Rom "point + tangent" vector to something based on an "all coordinates" vector, we need to determine the mapping matrix such that applying <em>T</em> yields P2 as start point, P3 as end point, and two tangents based on the lines between P1 and P3, and P2 nd P4, respectively.</p>
|
||||
<p>Computing <em>T</em> is really more "arranging the numbers":</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg" width="591px" height="83px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg" width="621px" height="79px" loading="lazy">
|
||||
<p>Thus:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg" width="143px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg" width="145px" height="76px" loading="lazy">
|
||||
<p>However, we're not <em>quite</em> done, because Catmull-Rom curves have a parameter called "tension", written as τ ("tau"), which is a scaling factor for the tangent vectors: the bigger the tension, the smaller the tangents, and the smaller the tension, the bigger the tangents. As such, the tension factor goes in the denominator for the tangents, and before we continue, let's add that tension factor into both our coordinate vector representation, and mapping matrix <em>T</em>:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg" width="285px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg" width="291px" height="79px" loading="lazy">
|
||||
<p>With the mapping matrix properly done, let's rewrite the "point + tangent" Catmull-Rom matrix form to a matrix form in terms of four coordinates, and see what we end up with:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="409px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="423px" height="75px" loading="lazy">
|
||||
<p>Replace point/tangent vector with the expression for all-coordinates:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg" width="549px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg" width="564px" height="76px" loading="lazy">
|
||||
<p>and merge the matrices:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg" width="455px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg" width="465px" height="76px" loading="lazy">
|
||||
<p>This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg" width="353px" height="73px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg" width="359px" height="75px" loading="lazy">
|
||||
<p>So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg" width="227px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg" width="227px" height="76px" loading="lazy">
|
||||
<p>Into something that looks like this:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg" width="167px" height="73px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg" width="171px" height="75px" loading="lazy">
|
||||
<p>And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg" width="440px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg" width="452px" height="76px" loading="lazy">
|
||||
<p>Then we remove the coordinate vector from both sides without affecting the equality:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg" width="353px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg" width="356px" height="76px" loading="lazy">
|
||||
<p>Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg" width="657px" height="88px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg" width="672px" height="84px" loading="lazy">
|
||||
<p>A matrix times its inverse is the matrix equivalent of 1, and because "something times 1" is the same as "something", so we can just outright remove any matrix/inverse pair:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg" width="369px" height="88px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg" width="372px" height="84px" loading="lazy">
|
||||
<p>And now we're <em>basically</em> done. We just multiply those two matrices and we know what <em>V</em> is:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg" width="161px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg" width="160px" height="73px" loading="lazy">
|
||||
<p>We now have the final piece of our function puzzle. Let's run through each step.</p>
|
||||
<ol>
|
||||
<li>Start with the Catmull-Rom function:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="409px" height="75px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg" width="423px" height="75px" loading="lazy">
|
||||
<ol start="2">
|
||||
<li>rewrite to pure coordinate form:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg" width="324px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg" width="329px" height="79px" loading="lazy">
|
||||
<ol start="3">
|
||||
<li>rewrite for "normal" coordinate vector:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg" width="441px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg" width="448px" height="76px" loading="lazy">
|
||||
<ol start="4">
|
||||
<li>merge the inner matrices:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg" width="348px" height="84px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg" width="348px" height="76px" loading="lazy">
|
||||
<ol start="5">
|
||||
<li>rewrite for Bézier matrix form:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg" width="431px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg" width="436px" height="75px" loading="lazy">
|
||||
<ol start="6">
|
||||
<li>and transform the coordinates so we have a "pure" Bézier expression:</li>
|
||||
</ol>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg" width="348px" height="81px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg" width="353px" height="77px" loading="lazy">
|
||||
<p>And we're done: we finally know how to convert these two curves!</p>
|
||||
</div>
|
||||
|
||||
<p>If we have a Catmull-Rom curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, then we can draw that curve using a Bézier curve that has the vector:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg" width="249px" height="85px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg" width="268px" height="81px" loading="lazy">
|
||||
<p>Similarly, if we have a Bézier curve defined by four coordinates P<sub>1</sub> through P<sub>4</sub>, we can draw that using a standard tension Catmull-Rom curve with the following coordinate values:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg" width="284px" height="77px" loading="lazy">
|
||||
<p>or, if your API requires specifying Catmull-Rom curves using "point + tangent" form:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg" width="284px" height="77px" loading="lazy">
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg" width="300px" height="79px" loading="lazy">
|
||||
<p>Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:</p>
|
||||
<img class="LaTeX SVG" src="./images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg" width="300px" height="80px" loading="lazy">
|
||||
|
||||
</section>
|
||||
<section id="catmullmolding">
|
||||
|
@@ -40,10 +40,12 @@ async function preprocessGraphicsElement(chapter, localeStrings, markdown) {
|
||||
// TODO: width/height attributes, but the graphics-element
|
||||
// does not, of course! [known bug]
|
||||
|
||||
let title = ``;
|
||||
|
||||
if (updated.indexOf(`width=`) === -1)
|
||||
updated = updated.replace(
|
||||
/title="([^"]+)"\s*/,
|
||||
`title="$1" width="275" `
|
||||
(_, t) => `title="${(title = t)}" width="275" `
|
||||
);
|
||||
|
||||
if (updated.indexOf(`height=`) === -1)
|
||||
@@ -95,8 +97,9 @@ async function preprocessGraphicsElement(chapter, localeStrings, markdown) {
|
||||
|
||||
const replacement = `width="${width}" height="${height}" src="${src}" ${remainder}>
|
||||
<fallback-image>
|
||||
<span class="view-source">${translate`disabledMessage`}</span>
|
||||
<img width="${width}px" height="${height}px" src="${imgUrl}">
|
||||
${translate`disabledMessage`}
|
||||
<label>${title}</label>
|
||||
</fallback-image>`;
|
||||
|
||||
updated = updated.replace(original, replacement);
|
||||
|