diff --git a/docs/chapters/catmullconv/catmull-rom.js b/docs/chapters/catmullconv/catmull-rom.js new file mode 100644 index 00000000..587b87b4 --- /dev/null +++ b/docs/chapters/catmullconv/catmull-rom.js @@ -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 { + 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(); + } +} diff --git a/docs/chapters/catmullconv/content.en-GB.md b/docs/chapters/catmullconv/content.en-GB.md index cbd4f509..4dbb291e 100644 --- a/docs/chapters/catmullconv/content.en-GB.md +++ b/docs/chapters/catmullconv/content.en-GB.md @@ -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 Px, has a tangent along the line Px-1 to Px+1. The curve runs from points P2 to Pn-1, 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. + + + -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 why 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!
@@ -539,14 +543,14 @@ Similarly, if we have a Bézier curve defined by four coordinates P1 \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} \] diff --git a/docs/chapters/curvefitting/curve-fitter.js b/docs/chapters/curvefitting/curve-fitter.js deleted file mode 100644 index cd7956ab..00000000 --- a/docs/chapters/curvefitting/curve-fitter.js +++ /dev/null @@ -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 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 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 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 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 = ``; @@ -100,14 +159,4 @@ setSliderValues(mode) { s.setAttribute(`value`, sliders.values[i]); s.value = sliders.values[i]; }); -} - -onMouseDown() { - if (!this.currentPoint) { - const {x, y} = this.cursor; - points.push({ x, y }); - resetMovable(points); - this.updateSliders(); - redraw(); - } -} +} \ No newline at end of file diff --git a/docs/chapters/curvefitting/matrix-invert.js b/docs/chapters/curvefitting/matrix-invert.js deleted file mode 100644 index 819e1d86..00000000 --- a/docs/chapters/curvefitting/matrix-invert.js +++ /dev/null @@ -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; -}; \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg b/docs/images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg index 8ca78170..05b1c458 100644 --- a/docs/images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg +++ b/docs/images/chapters/catmullconv/06ae1e3fdc660e59d618e0760e8e9ab5.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg b/docs/images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg index cdf87018..d3658c51 100644 --- a/docs/images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg +++ b/docs/images/chapters/catmullconv/169fd85a95e4d16fe289a75583017a11.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg b/docs/images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg index 05024a9b..592c6fe4 100644 --- a/docs/images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg +++ b/docs/images/chapters/catmullconv/1811b59c5ab9233f08590396e5d03303.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg b/docs/images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg index 67089bd3..2623d93e 100644 --- a/docs/images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg +++ b/docs/images/chapters/catmullconv/1b8a782f7540503d38067317e4cd00b0.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg b/docs/images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg index c3a8f54a..02741d1f 100644 --- a/docs/images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg +++ b/docs/images/chapters/catmullconv/26363fc09f8cf2d41ea5b4256656bb6d.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg b/docs/images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg index 5e0ad915..82a7eb32 100644 --- a/docs/images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg +++ b/docs/images/chapters/catmullconv/3ea54fe939d076f8db605c5b480e7db0.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg b/docs/images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg index 47af7bcd..bff03ed5 100644 --- a/docs/images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg +++ b/docs/images/chapters/catmullconv/4d524810417b4caffedd13af23135f5b.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg b/docs/images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg index aba2fde4..17e3d17f 100644 --- a/docs/images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg +++ b/docs/images/chapters/catmullconv/5f2750de827497375d9a915f96686885.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg b/docs/images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg index 48e02771..4380339b 100644 --- a/docs/images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg +++ b/docs/images/chapters/catmullconv/78ac9df086ec19147414359369b563fc.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg b/docs/images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg index 2a87af3b..534a46e7 100644 --- a/docs/images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg +++ b/docs/images/chapters/catmullconv/79e333cd0c569657eea033b04fb5e61b.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg b/docs/images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg index 29163cd8..6d38d835 100644 --- a/docs/images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg +++ b/docs/images/chapters/catmullconv/841fb6a2a035c9bcf5a2d46f2a67709b.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg b/docs/images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg index 6cec7f2d..641dbadb 100644 --- a/docs/images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg +++ b/docs/images/chapters/catmullconv/8f56909fcb62b8eef18b9b9559575c13.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg b/docs/images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg index 13c97bcb..064ccf2c 100644 --- a/docs/images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg +++ b/docs/images/chapters/catmullconv/9215d05705c8e8a7ebd718ae6f690371.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg b/docs/images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg index d0241723..8c1629da 100644 --- a/docs/images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg +++ b/docs/images/chapters/catmullconv/a47b072a325812ac4f0ff52c22792588.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/b0cb0cccdbea86dcdd610a871e86183f.png b/docs/images/chapters/catmullconv/b0cb0cccdbea86dcdd610a871e86183f.png new file mode 100644 index 00000000..106a47ba Binary files /dev/null and b/docs/images/chapters/catmullconv/b0cb0cccdbea86dcdd610a871e86183f.png differ diff --git a/docs/images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg b/docs/images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg index ad05befb..8b20d3e4 100644 --- a/docs/images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg +++ b/docs/images/chapters/catmullconv/b21386f86bef8894f108c5441dad10de.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg b/docs/images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg index 2ba18e0e..f1ffd3bf 100644 --- a/docs/images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg +++ b/docs/images/chapters/catmullconv/ba31c32eba62f1e3b15066cd5ddda597.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg b/docs/images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg index 21b428cf..ff0b3e57 100644 --- a/docs/images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg +++ b/docs/images/chapters/catmullconv/cbdd46d5e2e1a6202ef46fb03711ebe4.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg b/docs/images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg index 13c97bcb..064ccf2c 100644 --- a/docs/images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg +++ b/docs/images/chapters/catmullconv/cc1e2ff43350c32f0ae9ba9a7652b8fb.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg b/docs/images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg index e9f3d7e6..730f0ac2 100644 --- a/docs/images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg +++ b/docs/images/chapters/catmullconv/e3d30ab368dcead1411532ce3814d3f3.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg b/docs/images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg index ac806e12..e2692743 100644 --- a/docs/images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg +++ b/docs/images/chapters/catmullconv/eae7f01976e511ee38b08b6edc8765d2.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg b/docs/images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg index 5814d073..96d6d824 100644 --- a/docs/images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg +++ b/docs/images/chapters/catmullconv/f08e34395ce2812276fd70548f805041.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg b/docs/images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg index de8fc2b9..efd1b6ce 100644 --- a/docs/images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg +++ b/docs/images/chapters/catmullconv/f2b2a16a41d134ce0dfd544ab77ff25e.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg b/docs/images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg index ab576801..89aabbd7 100644 --- a/docs/images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg +++ b/docs/images/chapters/catmullconv/f41487aff3e34fafd5d4ee5979f133f1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg b/docs/images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg index 7030904e..4273506c 100644 --- a/docs/images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg +++ b/docs/images/chapters/catmullconv/f814bb8d627f9c8f33b347c1cf13d4c7.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/curvefitting/c2c92587a184efa6c4fee45e4a3e32ed.png b/docs/images/chapters/curvefitting/c6c8442e24793ce72a872ce29b2b4125.png similarity index 100% rename from docs/images/chapters/curvefitting/c2c92587a184efa6c4fee45e4a3e32ed.png rename to docs/images/chapters/curvefitting/c6c8442e24793ce72a872ce29b2b4125.png diff --git a/docs/index.html b/docs/index.html index 56f8d640..fddc89d0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -197,13 +197,15 @@
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -219,8 +221,9 @@

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -246,8 +249,9 @@

So, parametric curves don't define a y coordinate in terms of an x coordinate, like normal functions do, but they instead link the values to a "control" variable. If we vary the value of t, then with every change we get two values, which we can use as (x,y) coordinates in a graph. The above set of functions, for instance, generates points on a circle: We can range t from negative to positive infinity, and the resulting (x,y) coordinates will always lie on a circle with radius 1 around the origin (0,0). If we plot it for t from 0 to 5, we get this:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -328,24 +332,27 @@ function Bezier(3,t):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -359,8 +366,9 @@ function Bezier(3,t):

Which gives us the curve we saw at the top of the article:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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.

@@ -404,8 +412,9 @@ function Bezier(3,t,w[]):

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + ratio 1 1.0
ratio 2 1.0
@@ -462,13 +471,15 @@ function RationalBezier(3,t,w[],r[]):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -519,8 +530,9 @@ function RationalBezier(3,t,w[],r[]):

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -560,16 +572,18 @@ function RationalBezier(3,t,w[],r[]):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -604,8 +618,9 @@ function RationalBezier(3,t,w[],r[]):

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 t, the procedure gives us all the points we need to split a curve at that t 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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -727,8 +742,9 @@ function drawCurve(points[], t):

And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth 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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -802,13 +818,15 @@ treated as a sequence of three (elementary) shear operations. When we combine th
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -836,8 +854,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -910,8 +929,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -928,14 +948,16 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1070,15 +1092,17 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

And for cubic curves, that means first and second derivatives, in red and purple respectively:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1095,13 +1119,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1123,13 +1149,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1141,13 +1169,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1186,8 +1216,9 @@ function getCubicRoots(pa, pb, pc, pd) {

Taking that into account, we compute t, we disregard any t value that isn't in the Bézier interval [0,1], and we now know at which t value(s) our curve will inflect.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1198,8 +1229,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

This is a fairly funky image, so let's see what the various parts of it mean...

@@ -1264,8 +1296,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1275,8 +1308,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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 x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1302,8 +1336,9 @@ y = curve.get(t).y

So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1324,18 +1359,21 @@ y = curve.get(t).y
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1357,8 +1395,9 @@ y = curve.get(t).y

If we use the Legendre-Gauss values for our C values (thickness for each strip) and t 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?

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1370,16 +1409,18 @@ y = curve.get(t).y + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1425,8 +1466,9 @@ y = curve.get(t).y

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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:

@@ -1434,8 +1476,9 @@ y = curve.get(t).y

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1449,16 +1492,18 @@ y = curve.get(t).y

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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 t values, determine the distance-for-this-t-value at each point we generate during the run, and then we find "the closest t 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 t 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.

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 t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1476,8 +1521,9 @@ y = curve.get(t).y

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).

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1508,13 +1554,15 @@ lli = function(line1, line2):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1540,8 +1588,9 @@ lli = function(line1, line2):

(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1561,15 +1610,17 @@ lli = function(line1, line2): + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1615,16 +1666,18 @@ lli = function(line1, line2):

With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using A as our curve's control point:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.

That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

With that covered, we now also know the tangent line to our point B, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points e1 and e2 on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable t value, and then our e1 and e2 coordinates must obey the standard de Casteljau rule for linear interpolation:

@@ -1636,15 +1689,17 @@ lli = function(line1, line2):

The result of this approach looks as follows:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

It is important to remember that even though we're using a circular arc to come up with decent e1 and e2 terms, we're not trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; more on that later), we're only trying to come up with some reasonable e1 and e2 points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

That looks perfectly servicable!

@@ -1672,8 +1727,9 @@ for (coordinate, index) in LUT:

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1687,24 +1743,27 @@ for (coordinate, index) in LUT:

And we're done, because that's our new quadratic control point!

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

As before, cubic curves are a bit more work, because while it's easy to find our initial t value and ABC values, getting those all-important e1 and e2 coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate t value ourselves, which allowed us to find appropriate e1 and e2 coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own t value, and its own e1 and e2 values, and those may not make sense for the rest of the curve.

For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its t value and e1/e2 coordinates:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.

One way to combat this might be to combine the above approach with the approach from the creating curves section: generate both the "unchanged t/e1/e2" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised t value, and then interpolating between those two curves:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1788,8 +1847,9 @@ for (coordinate, index) in LUT:

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 floating point rounding errors to start making a difference (which is around 10~11 points).

- - Scripts are disabled. Showing fallback image. + Scripts are disabled. Showing fallback image. + +
@@ -1800,83 +1860,92 @@ for (coordinate, index) in LUT:

Bézier curves and Catmull-Rom curves

-

Taking an excursion to different splines, the other common design curve is the Catmull-Rom spline. Now, a Catmull-Rom spline is a form of 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?

-

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 Px, has a tangent along the line Px-1 to Px+1. The curve runs from points P2 to Pn-1, 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.

-

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.

-

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:

- -

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 why that conversion works, let's do some maths!

+

Taking an excursion to different splines, the other common design curve is the Catmull-Rom 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.

+ + + Scripts are disabled. Showing fallback image. + + + + + + +

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.

+

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, 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:

+ +

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!

Deriving the conversion formulae

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.

So, let's start with the first, where we want to satisfy the following equality:

- +

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 T 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.

Computing T is really more "arranging the numbers":

- +

Thus:

- +

However, we're not quite 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 T:

- +

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:

- +

Replace point/tangent vector with the expression for all-coordinates:

- +

and merge the matrices:

- +

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:

- +

So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:

- +

Into something that looks like this:

- +

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:

- +

Then we remove the coordinate vector from both sides without affecting the equality:

- +

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:

- +

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:

- +

And now we're basically done. We just multiply those two matrices and we know what V is:

- +

We now have the final piece of our function puzzle. Let's run through each step.

  1. Start with the Catmull-Rom function:
- +
  1. rewrite to pure coordinate form:
- +
  1. rewrite for "normal" coordinate vector:
- +
  1. merge the inner matrices:
- +
  1. rewrite for Bézier matrix form:
- +
  1. and transform the coordinates so we have a "pure" Bézier expression:
- +

And we're done: we finally know how to convert these two curves!

If we have a Catmull-Rom curve defined by four coordinates P1 through P4, then we can draw that curve using a Bézier curve that has the vector:

- +

Similarly, if we have a Bézier curve defined by four coordinates P1 through P4, we can draw that using a standard tension Catmull-Rom curve with the following coordinate values:

- -

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:

+
diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 1d95fdf8..e85b189b 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -200,13 +200,15 @@
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -222,8 +224,9 @@

では、実際に見てみましょう。下の図はインタラクティブになっています。上下キーで補間の比率が増減しますので、どうなるか確かめてみましょう。最初に3点があり、それを結んで2本の直線が引かれています。この直線の上でそれぞれ線形補間を行うと、2つの点が得られます。この2点の間でさらに線形補間を行うと、1つの点を得ることができます。そして、あらゆる比率に対して同様に点を求め、それをすべて集めると、このようにベジエ曲線ができるのです。

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -248,8 +251,9 @@

というわけで、普通の関数ではy座標をx座標によって定義しますが、パラメトリック曲線ではそうではなく、座標の値を「制御」変数と結びつけます。tの値を変化させるたびに2つの値が変化するので、これをグラフ上の座標 (x,y)として使うことができます。例えば、先ほどの関数の組は円周上の点を生成します。負の無限大から正の無限大へとtを動かすと、得られる座標(x,y)は常に中心(0,0)・半径1の円の上に乗ります。tを0から5まで変化させてプロットした場合は、このようになります。

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -330,24 +334,27 @@ function Bezier(3,t):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -402,8 +409,9 @@ function Bezier(3,t,w[]):

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + ratio 1 1.0
ratio 2 1.0
@@ -460,13 +468,15 @@ function RationalBezier(3,t,w[],r[]):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -517,8 +527,9 @@ function RationalBezier(3,t,w[],r[]):

下の図にマウスを乗せると、この様子を実際に見ることができます。ド・カステリョのアルゴリズムによって曲線上の点を明示的に計算していますが、マウスを動かすと求める点が変わります。マウスカーソルを左から右へ(もちろん、右から左へでも)動かせば、このアルゴリズムによって曲線が生成される様子がわかります。

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -557,16 +568,18 @@ function RationalBezier(3,t,w[],r[]):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -601,8 +614,9 @@ function RationalBezier(3,t,w[],r[]):

ベジエ曲線を分割して、繫ぎ合わせたときに元に戻るような小さい2曲線にしたい場合にも、ド・カステリョのアルゴリズムを使えば、これに必要な点をすべて求めることができます。ある値tに対してド・カステリョの骨格を組み立てると、そのtで曲線を分割する際に必要になる点がすべて得られます。骨格内部の点のうち、曲線上の点から見て手前側にある点によって一方の曲線が定義され、向こう側にある点によってもう一方の曲線が定義されます。

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -724,8 +738,9 @@ function drawCurve(points[], t):

And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth 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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -799,13 +814,15 @@ treated as a sequence of three (elementary) shear operations. When we combine th
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -833,8 +850,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -907,8 +925,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -925,14 +944,16 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1067,15 +1088,17 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

And for cubic curves, that means first and second derivatives, in red and purple respectively:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1092,13 +1115,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1120,13 +1145,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1138,13 +1165,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1183,8 +1212,9 @@ function getCubicRoots(pa, pb, pc, pd) {

Taking that into account, we compute t, we disregard any t value that isn't in the Bézier interval [0,1], and we now know at which t value(s) our curve will inflect.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1195,8 +1225,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

This is a fairly funky image, so let's see what the various parts of it mean...

@@ -1261,8 +1292,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1272,8 +1304,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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 x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1299,8 +1332,9 @@ y = curve.get(t).y

So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1321,18 +1355,21 @@ y = curve.get(t).y
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1354,8 +1391,9 @@ y = curve.get(t).y

If we use the Legendre-Gauss values for our C values (thickness for each strip) and t 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?

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1367,16 +1405,18 @@ y = curve.get(t).y + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1422,8 +1462,9 @@ y = curve.get(t).y

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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:

@@ -1431,8 +1472,9 @@ y = curve.get(t).y

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1446,16 +1488,18 @@ y = curve.get(t).y

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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 t values, determine the distance-for-this-t-value at each point we generate during the run, and then we find "the closest t 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 t 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.

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 t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1473,8 +1517,9 @@ y = curve.get(t).y

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).

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1505,13 +1550,15 @@ lli = function(line1, line2):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1537,8 +1584,9 @@ lli = function(line1, line2):

(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1558,15 +1606,17 @@ lli = function(line1, line2): + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1612,16 +1662,18 @@ lli = function(line1, line2):

With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using A as our curve's control point:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.

That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

With that covered, we now also know the tangent line to our point B, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points e1 and e2 on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable t value, and then our e1 and e2 coordinates must obey the standard de Casteljau rule for linear interpolation:

@@ -1633,15 +1685,17 @@ lli = function(line1, line2):

The result of this approach looks as follows:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

It is important to remember that even though we're using a circular arc to come up with decent e1 and e2 terms, we're not trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; more on that later), we're only trying to come up with some reasonable e1 and e2 points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

That looks perfectly servicable!

@@ -1669,8 +1723,9 @@ for (coordinate, index) in LUT:

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1684,24 +1739,27 @@ for (coordinate, index) in LUT:

And we're done, because that's our new quadratic control point!

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

As before, cubic curves are a bit more work, because while it's easy to find our initial t value and ABC values, getting those all-important e1 and e2 coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate t value ourselves, which allowed us to find appropriate e1 and e2 coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own t value, and its own e1 and e2 values, and those may not make sense for the rest of the curve.

For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its t value and e1/e2 coordinates:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.

One way to combat this might be to combine the above approach with the approach from the creating curves section: generate both the "unchanged t/e1/e2" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised t value, and then interpolating between those two curves:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1785,8 +1843,9 @@ for (coordinate, index) in LUT:

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 floating point rounding errors to start making a difference (which is around 10~11 points).

- - Scripts are disabled. Showing fallback image. + Scripts are disabled. Showing fallback image. + +
@@ -1797,83 +1856,92 @@ for (coordinate, index) in LUT:

Bézier curves and Catmull-Rom curves

-

Taking an excursion to different splines, the other common design curve is the Catmull-Rom spline. Now, a Catmull-Rom spline is a form of 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?

-

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 Px, has a tangent along the line Px-1 to Px+1. The curve runs from points P2 to Pn-1, 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.

-

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.

-

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:

- -

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 why that conversion works, let's do some maths!

+

Taking an excursion to different splines, the other common design curve is the Catmull-Rom 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.

+ + + Scripts are disabled. Showing fallback image. + + + + + + +

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.

+

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, 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:

+ +

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!

Deriving the conversion formulae

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.

So, let's start with the first, where we want to satisfy the following equality:

- +

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 T 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.

Computing T is really more "arranging the numbers":

- +

Thus:

- +

However, we're not quite 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 T:

- +

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:

- +

Replace point/tangent vector with the expression for all-coordinates:

- +

and merge the matrices:

- +

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:

- +

So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:

- +

Into something that looks like this:

- +

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:

- +

Then we remove the coordinate vector from both sides without affecting the equality:

- +

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:

- +

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:

- +

And now we're basically done. We just multiply those two matrices and we know what V is:

- +

We now have the final piece of our function puzzle. Let's run through each step.

  1. Start with the Catmull-Rom function:
- +
  1. rewrite to pure coordinate form:
- +
  1. rewrite for "normal" coordinate vector:
- +
  1. merge the inner matrices:
- +
  1. rewrite for Bézier matrix form:
- +
  1. and transform the coordinates so we have a "pure" Bézier expression:
- +

And we're done: we finally know how to convert these two curves!

If we have a Catmull-Rom curve defined by four coordinates P1 through P4, then we can draw that curve using a Bézier curve that has the vector:

- +

Similarly, if we have a Bézier curve defined by four coordinates P1 through P4, we can draw that using a standard tension Catmull-Rom curve with the following coordinate values:

- -

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:

+
diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 2e9e1298..dee015e7 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -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, diff --git a/docs/js/custom-element/api/types/bezier.js b/docs/js/custom-element/api/types/bezier.js index be10c47f..4ffc1dc4 100644 --- a/docs/js/custom-element/api/types/bezier.js +++ b/docs/js/custom-element/api/types/bezier.js @@ -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( diff --git a/docs/js/custom-element/api/util/matrix.js b/docs/js/custom-element/api/types/matrix.js similarity index 87% rename from docs/js/custom-element/api/util/matrix.js rename to docs/js/custom-element/api/types/matrix.js index 9fc40c88..cc1e9963 100644 --- a/docs/js/custom-element/api/util/matrix.js +++ b/docs/js/custom-element/api/types/matrix.js @@ -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)); } diff --git a/docs/js/custom-element/api/util/binomial.js b/docs/js/custom-element/api/util/binomial.js new file mode 100644 index 00000000..ae10c7f3 --- /dev/null +++ b/docs/js/custom-element/api/util/binomial.js @@ -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; diff --git a/docs/js/custom-element/api/util/fit-curve-to-points.js b/docs/js/custom-element/api/util/fit-curve-to-points.js new file mode 100644 index 00000000..5c2c38d8 --- /dev/null +++ b/docs/js/custom-element/api/util/fit-curve-to-points.js @@ -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 }; diff --git a/docs/js/custom-element/graphics-element.css b/docs/js/custom-element/graphics-element.css index 06fa603a..29255bdd 100644 --- a/docs/js/custom-element/graphics-element.css +++ b/docs/js/custom-element/graphics-element.css @@ -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; } /* diff --git a/docs/js/custom-element/graphics-element.js b/docs/js/custom-element/graphics-element.js index e40a4af5..3b9c0164 100644 --- a/docs/js/custom-element/graphics-element.js +++ b/docs/js/custom-element/graphics-element.js @@ -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 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 }; diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index c7c20560..f22bd465 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -194,13 +194,15 @@
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -217,8 +219,9 @@ : + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

@@ -242,8 +245,9 @@

所以,参数曲线不像一般函数那样,通过x坐标来定义y坐标,而是用一个“控制”变量将它们连接起来。如果改变t的值,每次变化时我们都能得到两个值,这可以作为图形中的(x,y)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使t在正负极值间变化,得到的输出(x,y)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出t从0到5时的值,将得到如下图像:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -324,24 +328,27 @@ function Bezier(3,t):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -396,8 +403,9 @@ function Bezier(3,t,w[]):

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + ratio 1 1.0
ratio 2 1.0
@@ -454,13 +462,15 @@ function RationalBezier(3,t,w[],r[]):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -511,8 +521,9 @@ function RationalBezier(3,t,w[],r[]):

我们通过实际操作来观察这个过程。在以下的图表中,移动鼠标来改变用de Casteljau算法计算得到的曲线点,左右移动鼠标,可以实时看到曲线是如何生成的。

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -551,16 +562,18 @@ function RationalBezier(3,t,w[],r[]):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -595,8 +608,9 @@ function RationalBezier(3,t,w[],r[]):

使用 de Casteljau 算法我们也可以将一条贝塞尔曲线分割成两条更小的曲线,二者拼接起来即可形成原来的曲线。当采用某个 t 值构造 de Casteljau 算法时,该过程会给到我们在 t 点分割曲线的所有点: 一条曲线包含该曲线上点之前的所有点,另一条曲线包含该曲线上点之后的所有点。

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -718,8 +732,9 @@ function drawCurve(points[], t):

And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth 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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -793,13 +808,15 @@ treated as a sequence of three (elementary) shear operations. When we combine th
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -827,8 +844,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -901,8 +919,9 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -919,14 +938,16 @@ treated as a sequence of three (elementary) shear operations. When we combine th

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1061,15 +1082,17 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

And for cubic curves, that means first and second derivatives, in red and purple respectively:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1086,13 +1109,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1114,13 +1139,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1132,13 +1159,15 @@ function getCubicRoots(pa, pb, pc, pd) {
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1177,8 +1206,9 @@ function getCubicRoots(pa, pb, pc, pd) {

Taking that into account, we compute t, we disregard any t value that isn't in the Bézier interval [0,1], and we now know at which t value(s) our curve will inflect.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1189,8 +1219,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

This is a fairly funky image, so let's see what the various parts of it mean...

@@ -1255,8 +1286,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1266,8 +1298,9 @@ function getCubicRoots(pa, pb, pc, pd) {

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 x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1293,8 +1326,9 @@ y = curve.get(t).y

So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1315,18 +1349,21 @@ y = curve.get(t).y
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1348,8 +1385,9 @@ y = curve.get(t).y

If we use the Legendre-Gauss values for our C values (thickness for each strip) and t 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?

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1361,16 +1399,18 @@ y = curve.get(t).y + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1416,8 +1456,9 @@ y = curve.get(t).y

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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:

@@ -1425,8 +1466,9 @@ y = curve.get(t).y

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:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1440,16 +1482,18 @@ y = curve.get(t).y

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

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 t values, determine the distance-for-this-t-value at each point we generate during the run, and then we find "the closest t 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 t 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.

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 t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1467,8 +1511,9 @@ y = curve.get(t).y

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).

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1499,13 +1544,15 @@ lli = function(line1, line2):
+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +
@@ -1531,8 +1578,9 @@ lli = function(line1, line2):

(can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1552,15 +1600,17 @@ lli = function(line1, line2): + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + + Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1606,16 +1656,18 @@ lli = function(line1, line2):

With this code in place, creating a quadratic curve from three points is literally just computing the ABC values, and using A as our curve's control point:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

For cubic curves we need to do a little more work, but really only just a little. We're first going to assume that a decent curve through the three points should approximate a circular arc, which first requires knowing how to fit a circle to three points. You may remember (if you ever learned it!) that a line between two points on a circle is called a chord, and that one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.

That means that if we have have three points on a circle, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle: if we find two of those lines, then their intersection will be our circle's center, and the circle's radius will—by definition!—be the distance from the center to any of our three points:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

With that covered, we now also know the tangent line to our point B, because the tangent to any point on the circle is a line through that point, perpendicular to the line from that point to the center. That just leaves marking appropriate points e1 and e2 on that tangent, so that we can construct a new cubic curve hull. We use the approach as we did for quadratic curves to automatically determine a reasonable t value, and then our e1 and e2 coordinates must obey the standard de Casteljau rule for linear interpolation:

@@ -1627,15 +1679,17 @@ lli = function(line1, line2):

The result of this approach looks as follows:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

It is important to remember that even though we're using a circular arc to come up with decent e1 and e2 terms, we're not trying to perfectly create a circular arc with a cubic curve (which is good, because we can't; more on that later), we're only trying to come up with some reasonable e1 and e2 points so we can construct a new cubic curve... so now that we have those: let's see what kind of cubic curve that gives us:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

That looks perfectly servicable!

@@ -1663,8 +1717,9 @@ for (coordinate, index) in LUT:

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.

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1678,24 +1733,27 @@ for (coordinate, index) in LUT:

And we're done, because that's our new quadratic control point!

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

As before, cubic curves are a bit more work, because while it's easy to find our initial t value and ABC values, getting those all-important e1 and e2 coordinates is going to pose a bit of a problem... in the section on curve creation, we were free to pick an appropriate t value ourselves, which allowed us to find appropriate e1 and e2 coordinates. That's great, but when we're curve molding we don't have that luxury: whatever point we decide to start moving around already has its own t value, and its own e1 and e2 values, and those may not make sense for the rest of the curve.

For example, let's see what happens if we just "go with what we get" when we pick a point and start moving it around, preserving its t value and e1/e2 coordinates:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. +

That looks reasonable, close to the original point, but the further we drag our point, the less "useful" things become. Especially if we drag our point across the baseline, rather than turning into a nice curve.

One way to combat this might be to combine the above approach with the approach from the creating curves section: generate both the "unchanged t/e1/e2" curve, as well as the "idealised" curve through the start/cursor/end points, with idealised t value, and then interpolating between those two curves:

+ Scripts are disabled. Showing fallback image. - Scripts are disabled. Showing fallback image. + @@ -1779,8 +1837,9 @@ for (coordinate, index) in LUT:

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 floating point rounding errors to start making a difference (which is around 10~11 points).

- - Scripts are disabled. Showing fallback image. + Scripts are disabled. Showing fallback image. + +
@@ -1791,83 +1850,92 @@ for (coordinate, index) in LUT:

Bézier curves and Catmull-Rom curves

-

Taking an excursion to different splines, the other common design curve is the Catmull-Rom spline. Now, a Catmull-Rom spline is a form of 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?

-

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 Px, has a tangent along the line Px-1 to Px+1. The curve runs from points P2 to Pn-1, 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.

-

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.

-

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:

- -

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 why that conversion works, let's do some maths!

+

Taking an excursion to different splines, the other common design curve is the Catmull-Rom 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.

+ + + Scripts are disabled. Showing fallback image. + + + + + + +

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.

+

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, 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:

+ +

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!

Deriving the conversion formulae

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.

So, let's start with the first, where we want to satisfy the following equality:

- +

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 T 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.

Computing T is really more "arranging the numbers":

- +

Thus:

- +

However, we're not quite 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 T:

- +

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:

- +

Replace point/tangent vector with the expression for all-coordinates:

- +

and merge the matrices:

- +

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:

- +

So, if we want to express a Catmull-Rom curve using a Bézier curve, we'll need to turn this Catmull-Rom bit:

- +

Into something that looks like this:

- +

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:

- +

Then we remove the coordinate vector from both sides without affecting the equality:

- +

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:

- +

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:

- +

And now we're basically done. We just multiply those two matrices and we know what V is:

- +

We now have the final piece of our function puzzle. Let's run through each step.

  1. Start with the Catmull-Rom function:
- +
  1. rewrite to pure coordinate form:
- +
  1. rewrite for "normal" coordinate vector:
- +
  1. merge the inner matrices:
- +
  1. rewrite for Bézier matrix form:
- +
  1. and transform the coordinates so we have a "pure" Bézier expression:
- +

And we're done: we finally know how to convert these two curves!

If we have a Catmull-Rom curve defined by four coordinates P1 through P4, then we can draw that curve using a Bézier curve that has the vector:

- +

Similarly, if we have a Bézier curve defined by four coordinates P1 through P4, we can draw that using a standard tension Catmull-Rom curve with the following coordinate values:

- -

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:

+
diff --git a/src/build/markdown/preprocess-graphics-element.js b/src/build/markdown/preprocess-graphics-element.js index 5cb56511..baee2f16 100644 --- a/src/build/markdown/preprocess-graphics-element.js +++ b/src/build/markdown/preprocess-graphics-element.js @@ -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}> + ${translate`disabledMessage`} - ${translate`disabledMessage`} + `; updated = updated.replace(original, replacement);