From 3d18403f799ea900c87f9f9b995ee3344085a111 Mon Sep 17 00:00:00 2001 From: Pomax Date: Tue, 18 Aug 2020 09:26:41 -0700 Subject: [PATCH] we don't need B''(t) for extrema --- chapters/aligning/content.en-GB.md | 4 +- chapters/aligning/cubic.js | 7 + chapters/aligning/quadratic.js | 7 + chapters/extremities/content.en-GB.md | 63 +++-- chapters/extremities/quadratic.js | 4 +- .../74a3ba53c0d1a79938bd2b1b8d57497d.png | Bin 0 -> 967 bytes .../4723d5fb04fe6aa379f7a73f7d251c84.png | Bin 0 -> 21536 bytes .../1c0367fad2a0d6946db1f55a8520793a.svg | 1 + .../6db78123d4b676ffdf85d53670c77468.svg | 1 + .../c621cc41f6f22ee1beedbcb510fa5b6b.svg | 1 + index.html | 266 ++++++++++++------ ja-JP/index.html | 266 ++++++++++++------ tools/build/markdown/convert-markdown.js | 12 +- .../markdown/preprocess-graphics-element.js | 3 +- zh-CN/index.html | 266 ++++++++++++------ 15 files changed, 597 insertions(+), 304 deletions(-) create mode 100644 chapters/aligning/cubic.js create mode 100644 chapters/aligning/quadratic.js create mode 100644 images/chapters/aligning/74a3ba53c0d1a79938bd2b1b8d57497d.png create mode 100644 images/chapters/extremities/4723d5fb04fe6aa379f7a73f7d251c84.png create mode 100644 images/latex/1c0367fad2a0d6946db1f55a8520793a.svg create mode 100644 images/latex/6db78123d4b676ffdf85d53670c77468.svg create mode 100644 images/latex/c621cc41f6f22ee1beedbcb510fa5b6b.svg diff --git a/chapters/aligning/content.en-GB.md b/chapters/aligning/content.en-GB.md index c8a312a7..69629d4c 100644 --- a/chapters/aligning/content.en-GB.md +++ b/chapters/aligning/content.en-GB.md @@ -40,5 +40,5 @@ If we drop all the zero-terms, this gives us: We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae: - - + + diff --git a/chapters/aligning/cubic.js b/chapters/aligning/cubic.js new file mode 100644 index 00000000..e264f926 --- /dev/null +++ b/chapters/aligning/cubic.js @@ -0,0 +1,7 @@ +setup() { + +} + +draw() { + clear(); +} diff --git a/chapters/aligning/quadratic.js b/chapters/aligning/quadratic.js new file mode 100644 index 00000000..e264f926 --- /dev/null +++ b/chapters/aligning/quadratic.js @@ -0,0 +1,7 @@ +setup() { + +} + +draw() { + clear(); +} diff --git a/chapters/extremities/content.en-GB.md b/chapters/extremities/content.en-GB.md index 982b5dec..7b963e83 100644 --- a/chapters/extremities/content.en-GB.md +++ b/chapters/extremities/content.en-GB.md @@ -1,31 +1,42 @@ # Finding extremities: root finding -Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding maxima and minima on the component functions, by solving the equations B'(t) = 0 and B''(t) = 0. That said, in the case of quadratic curves there is no B''(t), so we only need to compute B'(t) = 0. So, how do we compute the first and second derivatives? Fairly easily, actually, until our derivatives are 4th order or higher... then things get really hard. But let's start simple: +Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding maxima and minima on the component functions, by solving the equation B'(t) = 0. We've already seen that the derivative of a Bézier curve is a simpler Bézier curve, but how do we solve the equality? Fairly easily, actually, until our derivatives are 4th order or higher... then things get really hard. But let's start simple: ### Quadratic curves: linear derivatives. -Finding the solution for "where is this line 0" should be trivial: +The derivative of a quadratic Bézier curve is a linear Bézier curve, interpolating between just two terms, which means finding the solution for "where is this line 0" is effectively trivial by rewriting it to a function of `t` and solving. First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the [derivatives section](#derivatives): \[ \begin{aligned} - l(x) = ax + b &= 0,\\ - ax + b &= 0,\\ - ax &= -b \\ - x &= \frac{-b}{a} + B'(t) = a(1-t) + b(t) &= 0,\\ + a - at + bt &= 0,\\ + (b-a)t + a &= 0\\ \end{aligned} \] -Done. And quadratic curves have no meaningful second derivative, so we're *really* done. +And then we turn this into our solution for `t` using basic arithmetics: + +\[ +\begin{aligned} + (b-a)t + a &= 0,\\ + (b-a)t &= -a,\\ + t &= \frac{-a}{b-a}\\ +\end{aligned} +\] + +Done. + +Although with the [caveat](https://en.wikipedia.org/wiki/Caveat_emptor#Caveat_lector) that if `b-a` is zero, there is no solution and we probably shouldn't try to perform that division. ### Cubic curves: the quadratic formula. -The derivative of a cubic curve is a quadratic curve, and finding the roots for a quadratic Bézier curve means we can apply the [Quadratic formula](https://en.wikipedia.org/wiki/Quadratic_formula). If you've seen it before, you'll remember it, and if you haven't, it looks like this: +The derivative of a cubic Bézier curve is a quadratic Bézier curve, and finding the roots for a quadratic polynomial means we can apply the [Quadratic formula](https://en.wikipedia.org/wiki/Quadratic_formula). If you've seen it before, you'll remember it, and if you haven't, it looks like this: \[ Given\ f(t) = at^2 + bt + c,\ f(t)=0\ when\ t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \] -So, if we can express a Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that come out (because of that plus/minus sign we get two). Any value between 0 and 1 is a root that matters for Bézier curves, anything below or above that is irrelevant (because Bézier curves are only defined over the interval [0,1]). So, how do we convert? +So, if we can rewrite the Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that come out (because of that plus/minus sign we get two). Any value between 0 and 1 is a root that matters for Bézier curves, anything below or above that is irrelevant (because Bézier curves are only defined over the interval [0,1]). So, how do we convert? First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the [derivatives section](#derivatives): @@ -48,7 +59,7 @@ And then, using these *v* values, we can find out what our *a*, *b*, and *c* sho \end{aligned} \] -This gives us thee coefficients *a*, *b*, and *c* that are expressed in terms of *v* values, where the *v* values are just convenient expressions of our original *p* values, so we can do some trivial substitution to get: +This gives us three coefficients {a, b, c} that are expressed in terms of `v` values, where the `v` values are expressions of our original coordinate values, so we can do some substitution to get: \[ \begin{aligned} @@ -58,12 +69,14 @@ This gives us thee coefficients *a*, *b*, and *c* that are expressed in terms of \end{aligned} \] -Easy-peasy. We can now almost trivially find the roots by plugging those values into the quadratic formula. We also note that the second derivative of a cubic curve means computing the first derivative of a quadratic curve, and we just saw how to do that in the section above. +Easy-peasy. We can now almost trivially find the roots by plugging those values into the quadratic formula. ### Quartic curves: Cardano's algorithm. -Quartic—fourth degree—curves have a cubic function as derivative. Now, cubic functions are a bit of a problem because they're really hard to solve. But, way back in the 16th century, [Gerolamo Cardano](https://en.wikipedia.org/wiki/Gerolamo_Cardano) figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots is "easy", and then the only hard part is figuring out how to go from that form to the -generic form. So: +We haven't really looked at them before now, but the next step up would be a Quartic curve, a fourth degree Bézier curve. As expected, these have a derivative that is a cubic function, and now things get much harder. Cubic functions don't have a "simple" rule to find their roots, like the quadratic formula, and instead require quite a bit of rewriting to a form that we can even start to try to solve. + + +Back in the 16th century, before Bézier curves were a thing, and even before _calculus itself_ was a thing, [Gerolamo Cardano](https://en.wikipedia.org/wiki/Gerolamo_Cardano) figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots is "easier" (even if not "easy"): \[ \begin{aligned} @@ -72,9 +85,11 @@ generic form. So: \end{aligned} \] -This is easier because for the "easier formula" we can use [regular calculus](http://www.wolframalpha.com/input/?i=t^3+%2B+pt+%2B+q) to find the roots. (As a cubic function, however, it can have up to three roots, but two of those can be complex. For the purpose of Bézier curve extremities, we can completely ignore those complex roots, since our *t* is a plain real number from 0 to 1.) +We can see that the easier formula only has two constants, rather than four, and only two expressions involving `t`, rather than three: this makes things considerably easier to solve because it lets us use [regular calculus](http://www.wolframalpha.com/input/?i=t^3+%2B+pt+%2B+q) to find the values that satisfy the equasion. -So, the trick is to figure out how to turn the first formula into the second formula, and to then work out the maths that gives us the roots. This is explained in detail over at [Ken J. Ward's page](https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm) for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic equation, with the complex roots getting totally ignored. +Now, there is one small hitch: as a cubic function, the solutions may be [complex numbers](https://en.wikipedia.org/wiki/Complex_number) rather than plain numbers... And Cardona realised this, centuries befor complex numbers were a well-understood and established part of number theory. His interpretation of them was "these numbers are impossible but that's okay because they disappear again in later steps", allowing him to not think about them too much, but we have it even easier: as we're trying to find the roots for display purposes, we don't even _care_ about complex numbers: we're going to simplify Cardano's approach just that tiny bit further by throwing away any solution that's not a plain number. + +So, how do we rewrite the hard formula into the easier formula? This is explained in detail over at [Ken J. Ward's page](https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm) for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic equation, with the complex roots getting totally ignored, but if you're interested you should definitely head over to Ken's page and give the procedure a read-through.
@@ -168,27 +183,27 @@ function getCubicRoots(pa, pb, pc, pd) {
-And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to reduce recomputing things as much as possible" and now we have a way to find all roots for a cubic function and can just move on with using that to find extremities of our curves. +And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to prevent recomputing things as much as possible" and now we have a way to find all roots for a cubic function and can just move on with using that to find extremities of our curves. ### Quintic and higher order curves: finding numerical solutions -The problem with this is that as the order of the curve goes up, we can't actually solve those equations the normal way. We can't take the function, and then work out what the solutions are. Not to mention that even solving a third order derivative (for a fourth order curve) is already a royal pain in the backside. We need a better solution. We need numerical approaches. +And this is where thing stop, because we _cannot_ find the roots for polynomials of degree 5 or higher using algebra (a fact known as [the Abel–Ruffini theorem](https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem)). Instead, for occasions like these, where algebra simply cannot yield an answer, we turn to [numerical analysis](https://en.wikipedia.org/wiki/Numerical_analysis). -That's a fancy word for saying "rather than solve the function, treat the problem as a sequence of identical operations, the performing of which gets us closer and closer to the real answer". As it turns out, there is a really nice numerical root-finding algorithm, called the [Newton-Raphson](http://en.wikipedia.org/wiki/Newton-Raphson) root finding method (yes, after *[that](https://en.wikipedia.org/wiki/Isaac_Newton)* Newton), which we can make use of. +That's a fancy term for saying "rather than trying to find exact answers by manipulating symbols, find approximate answers by describing the underlying process as a combination of steps, each of which _can_ be assigned a number via symbolic manipulation". For example, trying to mathematically compute how much water fits in a completely crazy three dimensional shape is very hard, even if it got you the perfect, precise answer. A much easier approach, which would be less perfect but still entirely useful, would be to just grab a buck and start filling the shape until it was full: just count the number of buckets of water you used. And if we want a more precise answer, we can use smaller buckets. -The Newton-Raphson approach consists of picking a value *t* (any value will do), and getting the corresponding value of the function at that *t* value. For normal functions, we can treat that value as a height. If the height is zero, we're done, we have found a root. If it's not, we take the tangent of the curve at that point, and extend it until it passes the x-axis, which will be at some new point *t*. We then repeat the procedure with this new value, and we keep doing this until we find our root. +So that's what we're going to do here, too: we're going to treat the problem as a sequence of steps, and the smaller we can make each step, the closer we'll get to that "perfect, precise" answer. And as it turns out, there is a really nice numerical root-finding algorithm, called the [Newton-Raphson](http://en.wikipedia.org/wiki/Newton-Raphson) root finding method (yes, after *[that](https://en.wikipedia.org/wiki/Isaac_Newton)* Newton), which we can make use of. The Newton-Raphson approach consists of taking our impossible-to-solve function `f(x)`, picking some intial value `x` (literally any value will do), and calculating `f(x)`. We can think of that value as the "height" of the function at `x`. If that height is zero, we're done, we have found a root. If it isn't, we calculate the tangent line at `f(x)` and calculate at which `x` value _its_ height is zero (which we've already seen is very easy). That will give us a new `x` and we repeat the process until we find a root. -Mathematically, this means that for some *t*, at step *n=1*, we perform the following calculation until *fy*(*t*) is zero, so that the next *t* is the same as the one we already have: +Mathematically, this means that for some `x`, at step `n=1`, we perform the following calculation until `fy(x)` is zero, so that the next `t` is the same as the one we already have: \[ - t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_n)} + x_{n+1} = x_n - \frac{f_y(x_n)}{f'_y(x_n)} \] -(The Wikipedia article has a decent animation for this process, so I'm not adding a sketch for that here unless there are requests for it) +(The Wikipedia article has a decent animation for this process, so I will not add a graphic for that here) -Now, this works well only if we can pick good starting points, and our curve is continuously differentiable and doesn't have oscillations. Glossing over the exact meaning of those terms, the curves we're dealing with conform to those constraints, so as long as we pick good starting points, this will work. So the question is: which starting points do we pick? +Now, this works well only if we can pick good starting points, and our curve is [continuously differentiable](https://en.wikipedia.org/wiki/Continuous_function) and doesn't have [oscillations](https://en.wikipedia.org/wiki/Oscillation_(mathematics)). Glossing over the exact meaning of those terms, the curves we're dealing with conform to those constraints, so as long as we pick good starting points, this will work. So the question is: which starting points do we pick? -As it turns out, Newton-Raphson is so blindingly fast, so we could get away with just not picking: we simply run the algorithm from *t=0* to *t=1* at small steps (say, 1/200th) and the result will be all the roots we want. Of course, this may pose problems for high order Bézier curves: 200 steps for a 200th order Bézier curve is going to go wrong, but that's okay: there is no reason, ever, to use Bézier curves of crazy high orders. You might use a fifth order curve to get the "nicest still remotely workable" approximation of a full circle with a single Bézier curve, that's pretty much as high as you'll ever need to go. +As it turns out, Newton-Raphson is so blindingly fast that we could get away with just not picking: we simply run the algorithm from *t=0* to *t=1* at small steps (say, 1/200th) and the result will be all the roots we want. Of course, this may pose problems for high order Bézier curves: 200 steps for a 200th order Bézier curve is going to go wrong, but that's okay: there is no reason (at least, none that I know of) to _ever_ use Bézier curves of crazy high orders. You might use a fifth order curve to get the "nicest still remotely workable" approximation of a full circle with a single Bézier curve, but that's pretty much as high as you'll ever need to go. ### In conclusion: diff --git a/chapters/extremities/quadratic.js b/chapters/extremities/quadratic.js index 19faf4ee..da7a1de6 100644 --- a/chapters/extremities/quadratic.js +++ b/chapters/extremities/quadratic.js @@ -62,9 +62,7 @@ plotDimension(dim, dimension) { // Is there a solution for B'(t) = 0? let dpoints = dimension.dpoints[0]; - let a = dpoints[1].y - dpoints[0].y; - let b = dpoints[0].y; - let t3 = -b / a; + let t3 = -dpoints[0].y / (dpoints[1].y - dpoints[0].y); // Is that solution a value in [0,1]? if (t3 > 0 && t3 < 1) { diff --git a/images/chapters/aligning/74a3ba53c0d1a79938bd2b1b8d57497d.png b/images/chapters/aligning/74a3ba53c0d1a79938bd2b1b8d57497d.png new file mode 100644 index 0000000000000000000000000000000000000000..ff7c4e9c54487e140d82a847b930139b21ef1e96 GIT binary patch literal 967 zcmeAS@N?(olHy`uVBq!ia0y~yU=#*n4mO~O=*mYsfD~Jjx4R3&e-K=-cll%n2Id-1 z7srr_Id88VTk)AyLiYkWWL)Oa@M4 vMxit&l`||JGdL%VN{j}{Xks8Q8*F5}9LA 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 PDP$#;B$Cfwzx#gxqP$!t literal 0 HcmV?d00001 diff --git a/images/latex/1c0367fad2a0d6946db1f55a8520793a.svg b/images/latex/1c0367fad2a0d6946db1f55a8520793a.svg new file mode 100644 index 00000000..d2524e6c --- /dev/null +++ b/images/latex/1c0367fad2a0d6946db1f55a8520793a.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/latex/6db78123d4b676ffdf85d53670c77468.svg b/images/latex/6db78123d4b676ffdf85d53670c77468.svg new file mode 100644 index 00000000..7ae2cd25 --- /dev/null +++ b/images/latex/6db78123d4b676ffdf85d53670c77468.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/latex/c621cc41f6f22ee1beedbcb510fa5b6b.svg b/images/latex/c621cc41f6f22ee1beedbcb510fa5b6b.svg new file mode 100644 index 00000000..f2a29a0c --- /dev/null +++ b/images/latex/c621cc41f6f22ee1beedbcb510fa5b6b.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html index 47647068..383a6b97 100644 --- a/index.html +++ b/index.html @@ -2908,30 +2908,52 @@ function drawCurve(points[], t): Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding maxima and minima on the component functions, by solving the - equations B'(t) = 0 and B''(t) = 0. That said, in the case of - quadratic curves there is no B''(t), so we only need to compute - B'(t) = 0. So, how do we compute the first and second derivatives? - Fairly easily, actually, until our derivatives are 4th order or - higher... then things get really hard. But let's start simple: + equation B'(t) = 0. We've already seen that the derivative of a + Bézier curve is a simpler Bézier curve, but how do we solve the + equality? Fairly easily, actually, until our derivatives are 4th + order or higher... then things get really hard. But let's start + simple:

Quadratic curves: linear derivatives.

- Finding the solution for "where is this line 0" should be trivial: + The derivative of a quadratic Bézier curve is a linear Bézier curve, + interpolating between just two terms, which means finding the + solution for "where is this line 0" is effectively trivial by + rewriting it to a function of t and solving. First we + turn our cubic Bézier function into a quadratic one, by following + the rule mentioned at the end of the + derivatives section:

- Done. And quadratic curves have no meaningful second derivative, so - we're really done. + And then we turn this into our solution for t using + basic arithmetics: +

+ +

Done.

+

+ Although with the + caveat + that if b-a is zero, there is no solution and we + probably shouldn't try to perform that division.

Cubic curves: the quadratic formula.

- The derivative of a cubic curve is a quadratic curve, and finding - the roots for a quadratic Bézier curve means we can apply the + The derivative of a cubic Bézier curve is a quadratic Bézier curve, + and finding the roots for a quadratic polynomial means we can apply + the Quadratic formula. If you've seen it before, you'll remember it, and if you haven't, @@ -2944,7 +2966,7 @@ function drawCurve(points[], t): height="40px" />

- So, if we can express a Bézier component function as a plain + So, if we can rewrite the Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that @@ -2975,11 +2997,10 @@ function drawCurve(points[], t): height="119px" />

- This gives us thee coefficients a, b, and - c that are expressed in terms of v values, where - the v values are just convenient expressions of our - original p values, so we can do some trivial substitution - to get: + This gives us three coefficients {a, b, c} that are expressed in + terms of v values, where the v values are + expressions of our original coordinate values, so we can do some + substitution to get:

Easy-peasy. We can now almost trivially find the roots by plugging - those values into the quadratic formula. We also note that the - second derivative of a cubic curve means computing the first - derivative of a quadratic curve, and we just saw how to do that in - the section above. + those values into the quadratic formula.

Quartic curves: Cardano's algorithm.

- Quartic—fourth degree—curves have a cubic function as derivative. - Now, cubic functions are a bit of a problem because they're really - hard to solve. But, way back in the 16th century, + We haven't really looked at them before now, but the next step up + would be a Quartic curve, a fourth degree Bézier curve. As expected, + these have a derivative that is a cubic function, and now things get + much harder. Cubic functions don't have a "simple" rule to find + their roots, like the quadratic formula, and instead require quite a + bit of rewriting to a form that we can even start to try to solve. +

+

+ Back in the 16th century, before Bézier curves were a + thing, and even before calculus itself was a thing, Gerolamo Cardano figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots - is "easy", and then the only hard part is figuring out how to go - from that form to the generic form. So: + is "easier" (even if not "easy"):

- This is easier because for the "easier formula" we can use + We can see that the easier formula only has two constants, rather + than four, and only two expressions involving t, rather + than three: this makes things considerably easier to solve because + it lets us use regular calculus - to find the roots. (As a cubic function, however, it can have up to - three roots, but two of those can be complex. For the purpose of - Bézier curve extremities, we can completely ignore those complex - roots, since our t is a plain real number from 0 to 1.) + to find the values that satisfy the equasion.

- So, the trick is to figure out how to turn the first formula into - the second formula, and to then work out the maths that gives us the - roots. This is explained in detail over at + Now, there is one small hitch: as a cubic function, the solutions + may be + complex numbers + rather than plain numbers... And Cardona realised this, centuries + befor complex numbers were a well-understood and established part of + number theory. His interpretation of them was "these numbers are + impossible but that's okay because they disappear again in later + steps", allowing him to not think about them too much, but we have + it even easier: as we're trying to find the roots for display + purposes, we don't even care about complex numbers: we're + going to simplify Cardano's approach just that tiny bit further by + throwing away any solution that's not a plain number. +

+

+ So, how do we rewrite the hard formula into the easier formula? This + is explained in detail over at Ken J. Ward's page for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic - equation, with the complex roots getting totally ignored. + equation, with the complex roots getting totally ignored, but if + you're interested you should definitely head over to Ken's page and + give the procedure a read-through.

Implementing Cardano's algorithm for finding all real roots

@@ -3135,24 +3176,41 @@ function getCubicRoots(pa, pb, pc, pd) {

And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to - reduce recomputing things as much as possible" and now we have a way - to find all roots for a cubic function and can just move on with + prevent recomputing things as much as possible" and now we have a + way to find all roots for a cubic function and can just move on with using that to find extremities of our curves.

Quintic and higher order curves: finding numerical solutions

- The problem with this is that as the order of the curve goes up, we - can't actually solve those equations the normal way. We can't take - the function, and then work out what the solutions are. Not to - mention that even solving a third order derivative (for a fourth - order curve) is already a royal pain in the backside. We need a - better solution. We need numerical approaches. + And this is where thing stop, because we cannot find the + roots for polynomials of degree 5 or higher using algebra (a fact + known as + the Abel–Ruffini theorem). Instead, for occasions like these, where algebra simply cannot + yield an answer, we turn to + numerical analysis.

- That's a fancy word for saying "rather than solve the function, - treat the problem as a sequence of identical operations, the - performing of which gets us closer and closer to the real answer". - As it turns out, there is a really nice numerical root-finding + That's a fancy term for saying "rather than trying to find exact + answers by manipulating symbols, find approximate answers by + describing the underlying process as a combination of steps, each of + which can be assigned a number via symbolic manipulation". + For example, trying to mathematically compute how much water fits in + a completely crazy three dimensional shape is very hard, even if it + got you the perfect, precise answer. A much easier approach, which + would be less perfect but still entirely useful, would be to just + grab a buck and start filling the shape until it was full: just + count the number of buckets of water you used. And if we want a more + precise answer, we can use smaller buckets. +

+

+ So that's what we're going to do here, too: we're going to treat the + problem as a sequence of steps, and the smaller we can make each + step, the closer we'll get to that "perfect, precise" answer. And as + it turns out, there is a really nice numerical root-finding algorithm, called the Newton-Raphsonthat - Newton), which we can make use of. + Newton), which we can make use of. The Newton-Raphson approach + consists of taking our impossible-to-solve function + f(x), picking some intial value + x (literally any value will do), and calculating + f(x). We can think of that value as the "height" of the + function at x. If that height is zero, we're done, we + have found a root. If it isn't, we calculate the tangent line at + f(x) and calculate at which x value + its height is zero (which we've already seen is very easy). + That will give us a new x and we repeat the process + until we find a root.

- The Newton-Raphson approach consists of picking a value - t (any value will do), and getting the corresponding value - of the function at that t value. For normal functions, we - can treat that value as a height. If the height is zero, we're done, - we have found a root. If it's not, we take the tangent of the curve - at that point, and extend it until it passes the x-axis, which will - be at some new point t. We then repeat the procedure with - this new value, and we keep doing this until we find our root. -

-

- Mathematically, this means that for some t, at step - n=1, we perform the following calculation until - fy(t) is zero, so that the next t is the same as - the one we already have: + Mathematically, this means that for some x, at step + n=1, we perform the following calculation until + fy(x) is zero, so that the next + t is the same as the one we already have:

- (The Wikipedia article has a decent animation for this process, so - I'm not adding a sketch for that here unless there are requests for - it) + (The Wikipedia article has a decent animation for this process, so I + will not add a graphic for that here)

Now, this works well only if we can pick good starting points, and - our curve is continuously differentiable and doesn't have - oscillations. Glossing over the exact meaning of those terms, the - curves we're dealing with conform to those constraints, so as long - as we pick good starting points, this will work. So the question is: - which starting points do we pick? + our curve is + continuously differentiable + and doesn't have + oscillations. Glossing over the exact meaning of those terms, the curves we're + dealing with conform to those constraints, so as long as we pick + good starting points, this will work. So the question is: which + starting points do we pick?

- As it turns out, Newton-Raphson is so blindingly fast, so we could + As it turns out, Newton-Raphson is so blindingly fast that we could get away with just not picking: we simply run the algorithm from t=0 to t=1 at small steps (say, 1/200th) and the result will be all the roots we want. Of course, this may pose problems for high order Bézier curves: 200 steps for a 200th order Bézier curve is going to go - wrong, but that's okay: there is no reason, ever, to use Bézier - curves of crazy high orders. You might use a fifth order curve to - get the "nicest still remotely workable" approximation of a full - circle with a single Bézier curve, that's pretty much as high as - you'll ever need to go. + wrong, but that's okay: there is no reason (at least, none that I + know of) to ever use Bézier curves of crazy high orders. + You might use a fifth order curve to get the "nicest still remotely + workable" approximation of a full circle with a single Bézier curve, + but that's pretty much as high as you'll ever need to go.

In conclusion:

@@ -3228,7 +3290,7 @@ function getCubicRoots(pa, pb, pc, pd) { Scripts are disabled. Showing fallback image. @@ -3385,16 +3447,38 @@ function getCubicRoots(pa, pb, pc, pd) { aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae:

- - + + + Scripts are disabled. Showing fallback image. + + + width="275" + height="275" + src="./chapters/aligning/cubic.js" + > + + + Scripts are disabled. Showing fallback image. +

Tight boxes

diff --git a/ja-JP/index.html b/ja-JP/index.html index 97677003..a21a7c3e 100644 --- a/ja-JP/index.html +++ b/ja-JP/index.html @@ -2578,30 +2578,52 @@ function drawCurve(points[], t): Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding maxima and minima on the component functions, by solving the - equations B'(t) = 0 and B''(t) = 0. That said, in the case of - quadratic curves there is no B''(t), so we only need to compute - B'(t) = 0. So, how do we compute the first and second derivatives? - Fairly easily, actually, until our derivatives are 4th order or - higher... then things get really hard. But let's start simple: + equation B'(t) = 0. We've already seen that the derivative of a + Bézier curve is a simpler Bézier curve, but how do we solve the + equality? Fairly easily, actually, until our derivatives are 4th + order or higher... then things get really hard. But let's start + simple:

Quadratic curves: linear derivatives.

- Finding the solution for "where is this line 0" should be trivial: + The derivative of a quadratic Bézier curve is a linear Bézier curve, + interpolating between just two terms, which means finding the + solution for "where is this line 0" is effectively trivial by + rewriting it to a function of t and solving. First we + turn our cubic Bézier function into a quadratic one, by following + the rule mentioned at the end of the + derivatives section:

- Done. And quadratic curves have no meaningful second derivative, so - we're really done. + And then we turn this into our solution for t using + basic arithmetics: +

+ +

Done.

+

+ Although with the + caveat + that if b-a is zero, there is no solution and we + probably shouldn't try to perform that division.

Cubic curves: the quadratic formula.

- The derivative of a cubic curve is a quadratic curve, and finding - the roots for a quadratic Bézier curve means we can apply the + The derivative of a cubic Bézier curve is a quadratic Bézier curve, + and finding the roots for a quadratic polynomial means we can apply + the Quadratic formula. If you've seen it before, you'll remember it, and if you haven't, @@ -2614,7 +2636,7 @@ function drawCurve(points[], t): height="40px" />

- So, if we can express a Bézier component function as a plain + So, if we can rewrite the Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that @@ -2645,11 +2667,10 @@ function drawCurve(points[], t): height="119px" />

- This gives us thee coefficients a, b, and - c that are expressed in terms of v values, where - the v values are just convenient expressions of our - original p values, so we can do some trivial substitution - to get: + This gives us three coefficients {a, b, c} that are expressed in + terms of v values, where the v values are + expressions of our original coordinate values, so we can do some + substitution to get:

Easy-peasy. We can now almost trivially find the roots by plugging - those values into the quadratic formula. We also note that the - second derivative of a cubic curve means computing the first - derivative of a quadratic curve, and we just saw how to do that in - the section above. + those values into the quadratic formula.

Quartic curves: Cardano's algorithm.

- Quartic—fourth degree—curves have a cubic function as derivative. - Now, cubic functions are a bit of a problem because they're really - hard to solve. But, way back in the 16th century, + We haven't really looked at them before now, but the next step up + would be a Quartic curve, a fourth degree Bézier curve. As expected, + these have a derivative that is a cubic function, and now things get + much harder. Cubic functions don't have a "simple" rule to find + their roots, like the quadratic formula, and instead require quite a + bit of rewriting to a form that we can even start to try to solve. +

+

+ Back in the 16th century, before Bézier curves were a + thing, and even before calculus itself was a thing, Gerolamo Cardano figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots - is "easy", and then the only hard part is figuring out how to go - from that form to the generic form. So: + is "easier" (even if not "easy"):

- This is easier because for the "easier formula" we can use + We can see that the easier formula only has two constants, rather + than four, and only two expressions involving t, rather + than three: this makes things considerably easier to solve because + it lets us use regular calculus - to find the roots. (As a cubic function, however, it can have up to - three roots, but two of those can be complex. For the purpose of - Bézier curve extremities, we can completely ignore those complex - roots, since our t is a plain real number from 0 to 1.) + to find the values that satisfy the equasion.

- So, the trick is to figure out how to turn the first formula into - the second formula, and to then work out the maths that gives us the - roots. This is explained in detail over at + Now, there is one small hitch: as a cubic function, the solutions + may be + complex numbers + rather than plain numbers... And Cardona realised this, centuries + befor complex numbers were a well-understood and established part of + number theory. His interpretation of them was "these numbers are + impossible but that's okay because they disappear again in later + steps", allowing him to not think about them too much, but we have + it even easier: as we're trying to find the roots for display + purposes, we don't even care about complex numbers: we're + going to simplify Cardano's approach just that tiny bit further by + throwing away any solution that's not a plain number. +

+

+ So, how do we rewrite the hard formula into the easier formula? This + is explained in detail over at Ken J. Ward's page for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic - equation, with the complex roots getting totally ignored. + equation, with the complex roots getting totally ignored, but if + you're interested you should definitely head over to Ken's page and + give the procedure a read-through.

Implementing Cardano's algorithm for finding all real roots

@@ -2805,24 +2846,41 @@ function getCubicRoots(pa, pb, pc, pd) {

And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to - reduce recomputing things as much as possible" and now we have a way - to find all roots for a cubic function and can just move on with + prevent recomputing things as much as possible" and now we have a + way to find all roots for a cubic function and can just move on with using that to find extremities of our curves.

Quintic and higher order curves: finding numerical solutions

- The problem with this is that as the order of the curve goes up, we - can't actually solve those equations the normal way. We can't take - the function, and then work out what the solutions are. Not to - mention that even solving a third order derivative (for a fourth - order curve) is already a royal pain in the backside. We need a - better solution. We need numerical approaches. + And this is where thing stop, because we cannot find the + roots for polynomials of degree 5 or higher using algebra (a fact + known as + the Abel–Ruffini theorem). Instead, for occasions like these, where algebra simply cannot + yield an answer, we turn to + numerical analysis.

- That's a fancy word for saying "rather than solve the function, - treat the problem as a sequence of identical operations, the - performing of which gets us closer and closer to the real answer". - As it turns out, there is a really nice numerical root-finding + That's a fancy term for saying "rather than trying to find exact + answers by manipulating symbols, find approximate answers by + describing the underlying process as a combination of steps, each of + which can be assigned a number via symbolic manipulation". + For example, trying to mathematically compute how much water fits in + a completely crazy three dimensional shape is very hard, even if it + got you the perfect, precise answer. A much easier approach, which + would be less perfect but still entirely useful, would be to just + grab a buck and start filling the shape until it was full: just + count the number of buckets of water you used. And if we want a more + precise answer, we can use smaller buckets. +

+

+ So that's what we're going to do here, too: we're going to treat the + problem as a sequence of steps, and the smaller we can make each + step, the closer we'll get to that "perfect, precise" answer. And as + it turns out, there is a really nice numerical root-finding algorithm, called the Newton-Raphsonthat - Newton), which we can make use of. + Newton), which we can make use of. The Newton-Raphson approach + consists of taking our impossible-to-solve function + f(x), picking some intial value + x (literally any value will do), and calculating + f(x). We can think of that value as the "height" of the + function at x. If that height is zero, we're done, we + have found a root. If it isn't, we calculate the tangent line at + f(x) and calculate at which x value + its height is zero (which we've already seen is very easy). + That will give us a new x and we repeat the process + until we find a root.

- The Newton-Raphson approach consists of picking a value - t (any value will do), and getting the corresponding value - of the function at that t value. For normal functions, we - can treat that value as a height. If the height is zero, we're done, - we have found a root. If it's not, we take the tangent of the curve - at that point, and extend it until it passes the x-axis, which will - be at some new point t. We then repeat the procedure with - this new value, and we keep doing this until we find our root. -

-

- Mathematically, this means that for some t, at step - n=1, we perform the following calculation until - fy(t) is zero, so that the next t is the same as - the one we already have: + Mathematically, this means that for some x, at step + n=1, we perform the following calculation until + fy(x) is zero, so that the next + t is the same as the one we already have:

- (The Wikipedia article has a decent animation for this process, so - I'm not adding a sketch for that here unless there are requests for - it) + (The Wikipedia article has a decent animation for this process, so I + will not add a graphic for that here)

Now, this works well only if we can pick good starting points, and - our curve is continuously differentiable and doesn't have - oscillations. Glossing over the exact meaning of those terms, the - curves we're dealing with conform to those constraints, so as long - as we pick good starting points, this will work. So the question is: - which starting points do we pick? + our curve is + continuously differentiable + and doesn't have + oscillations. Glossing over the exact meaning of those terms, the curves we're + dealing with conform to those constraints, so as long as we pick + good starting points, this will work. So the question is: which + starting points do we pick?

- As it turns out, Newton-Raphson is so blindingly fast, so we could + As it turns out, Newton-Raphson is so blindingly fast that we could get away with just not picking: we simply run the algorithm from t=0 to t=1 at small steps (say, 1/200th) and the result will be all the roots we want. Of course, this may pose problems for high order Bézier curves: 200 steps for a 200th order Bézier curve is going to go - wrong, but that's okay: there is no reason, ever, to use Bézier - curves of crazy high orders. You might use a fifth order curve to - get the "nicest still remotely workable" approximation of a full - circle with a single Bézier curve, that's pretty much as high as - you'll ever need to go. + wrong, but that's okay: there is no reason (at least, none that I + know of) to ever use Bézier curves of crazy high orders. + You might use a fifth order curve to get the "nicest still remotely + workable" approximation of a full circle with a single Bézier curve, + but that's pretty much as high as you'll ever need to go.

In conclusion:

@@ -2898,7 +2960,7 @@ function getCubicRoots(pa, pb, pc, pd) { Scripts are disabled. Showing fallback image. @@ -3055,16 +3117,38 @@ function getCubicRoots(pa, pb, pc, pd) { aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae:

- - + + + Scripts are disabled. Showing fallback image. + + + width="275" + height="275" + src="./chapters/aligning/cubic.js" + > + + + Scripts are disabled. Showing fallback image. +

Tight boxes

diff --git a/tools/build/markdown/convert-markdown.js b/tools/build/markdown/convert-markdown.js index 993a499f..b36e3330 100644 --- a/tools/build/markdown/convert-markdown.js +++ b/tools/build/markdown/convert-markdown.js @@ -9,7 +9,17 @@ nunjucks.configure(".", { autoescape: false }); * ...docs go here... */ async function convertMarkDown(chapter, localeStrings, markdown) { - markdown = await preprocessGraphicsElement(chapter, localeStrings, markdown); + try { + markdown = await preprocessGraphicsElement( + chapter, + localeStrings, + markdown + ); + } catch (e) { + console.error(`Error in ${chapter}:${localeStrings.currentLocale}.`); + console.error(e); + process.exit(1); + } // This yields the original markdown with all LaTeX blocked replaced with // uniquely named templating variables, referencing keys in the `latex` array. diff --git a/tools/build/markdown/preprocess-graphics-element.js b/tools/build/markdown/preprocess-graphics-element.js index 54136037..e9e5c24b 100644 --- a/tools/build/markdown/preprocess-graphics-element.js +++ b/tools/build/markdown/preprocess-graphics-element.js @@ -34,7 +34,7 @@ async function preprocessGraphicsElement(chapter, localeStrings, markdown) { if (updated.indexOf(`height=`) === -1) updated = updated.replace( - /width="(\d+)\s*"/, + /width="(\d+)"\s*/, `width="$1" height="275" ` ); @@ -42,6 +42,7 @@ async function preprocessGraphicsElement(chapter, localeStrings, markdown) { const terms = updated.match( /width="([^"]+)"\s+height="([^"]+)"\s+src="([^"]+)"\s*>/ ); + const [original, width, height] = terms; let src = terms[3]; diff --git a/zh-CN/index.html b/zh-CN/index.html index 83f846dc..81409df8 100644 --- a/zh-CN/index.html +++ b/zh-CN/index.html @@ -2588,30 +2588,52 @@ function drawCurve(points[], t): Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding maxima and minima on the component functions, by solving the - equations B'(t) = 0 and B''(t) = 0. That said, in the case of - quadratic curves there is no B''(t), so we only need to compute - B'(t) = 0. So, how do we compute the first and second derivatives? - Fairly easily, actually, until our derivatives are 4th order or - higher... then things get really hard. But let's start simple: + equation B'(t) = 0. We've already seen that the derivative of a + Bézier curve is a simpler Bézier curve, but how do we solve the + equality? Fairly easily, actually, until our derivatives are 4th + order or higher... then things get really hard. But let's start + simple:

Quadratic curves: linear derivatives.

- Finding the solution for "where is this line 0" should be trivial: + The derivative of a quadratic Bézier curve is a linear Bézier curve, + interpolating between just two terms, which means finding the + solution for "where is this line 0" is effectively trivial by + rewriting it to a function of t and solving. First we + turn our cubic Bézier function into a quadratic one, by following + the rule mentioned at the end of the + derivatives section:

- Done. And quadratic curves have no meaningful second derivative, so - we're really done. + And then we turn this into our solution for t using + basic arithmetics: +

+ +

Done.

+

+ Although with the + caveat + that if b-a is zero, there is no solution and we + probably shouldn't try to perform that division.

Cubic curves: the quadratic formula.

- The derivative of a cubic curve is a quadratic curve, and finding - the roots for a quadratic Bézier curve means we can apply the + The derivative of a cubic Bézier curve is a quadratic Bézier curve, + and finding the roots for a quadratic polynomial means we can apply + the Quadratic formula. If you've seen it before, you'll remember it, and if you haven't, @@ -2624,7 +2646,7 @@ function drawCurve(points[], t): height="40px" />

- So, if we can express a Bézier component function as a plain + So, if we can rewrite the Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that @@ -2655,11 +2677,10 @@ function drawCurve(points[], t): height="119px" />

- This gives us thee coefficients a, b, and - c that are expressed in terms of v values, where - the v values are just convenient expressions of our - original p values, so we can do some trivial substitution - to get: + This gives us three coefficients {a, b, c} that are expressed in + terms of v values, where the v values are + expressions of our original coordinate values, so we can do some + substitution to get:

Easy-peasy. We can now almost trivially find the roots by plugging - those values into the quadratic formula. We also note that the - second derivative of a cubic curve means computing the first - derivative of a quadratic curve, and we just saw how to do that in - the section above. + those values into the quadratic formula.

Quartic curves: Cardano's algorithm.

- Quartic—fourth degree—curves have a cubic function as derivative. - Now, cubic functions are a bit of a problem because they're really - hard to solve. But, way back in the 16th century, + We haven't really looked at them before now, but the next step up + would be a Quartic curve, a fourth degree Bézier curve. As expected, + these have a derivative that is a cubic function, and now things get + much harder. Cubic functions don't have a "simple" rule to find + their roots, like the quadratic formula, and instead require quite a + bit of rewriting to a form that we can even start to try to solve. +

+

+ Back in the 16th century, before Bézier curves were a + thing, and even before calculus itself was a thing, Gerolamo Cardano figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots - is "easy", and then the only hard part is figuring out how to go - from that form to the generic form. So: + is "easier" (even if not "easy"):

- This is easier because for the "easier formula" we can use + We can see that the easier formula only has two constants, rather + than four, and only two expressions involving t, rather + than three: this makes things considerably easier to solve because + it lets us use regular calculus - to find the roots. (As a cubic function, however, it can have up to - three roots, but two of those can be complex. For the purpose of - Bézier curve extremities, we can completely ignore those complex - roots, since our t is a plain real number from 0 to 1.) + to find the values that satisfy the equasion.

- So, the trick is to figure out how to turn the first formula into - the second formula, and to then work out the maths that gives us the - roots. This is explained in detail over at + Now, there is one small hitch: as a cubic function, the solutions + may be + complex numbers + rather than plain numbers... And Cardona realised this, centuries + befor complex numbers were a well-understood and established part of + number theory. His interpretation of them was "these numbers are + impossible but that's okay because they disappear again in later + steps", allowing him to not think about them too much, but we have + it even easier: as we're trying to find the roots for display + purposes, we don't even care about complex numbers: we're + going to simplify Cardano's approach just that tiny bit further by + throwing away any solution that's not a plain number. +

+

+ So, how do we rewrite the hard formula into the easier formula? This + is explained in detail over at Ken J. Ward's page for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic - equation, with the complex roots getting totally ignored. + equation, with the complex roots getting totally ignored, but if + you're interested you should definitely head over to Ken's page and + give the procedure a read-through.

Implementing Cardano's algorithm for finding all real roots

@@ -2815,24 +2856,41 @@ function getCubicRoots(pa, pb, pc, pd) {

And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to - reduce recomputing things as much as possible" and now we have a way - to find all roots for a cubic function and can just move on with + prevent recomputing things as much as possible" and now we have a + way to find all roots for a cubic function and can just move on with using that to find extremities of our curves.

Quintic and higher order curves: finding numerical solutions

- The problem with this is that as the order of the curve goes up, we - can't actually solve those equations the normal way. We can't take - the function, and then work out what the solutions are. Not to - mention that even solving a third order derivative (for a fourth - order curve) is already a royal pain in the backside. We need a - better solution. We need numerical approaches. + And this is where thing stop, because we cannot find the + roots for polynomials of degree 5 or higher using algebra (a fact + known as + the Abel–Ruffini theorem). Instead, for occasions like these, where algebra simply cannot + yield an answer, we turn to + numerical analysis.

- That's a fancy word for saying "rather than solve the function, - treat the problem as a sequence of identical operations, the - performing of which gets us closer and closer to the real answer". - As it turns out, there is a really nice numerical root-finding + That's a fancy term for saying "rather than trying to find exact + answers by manipulating symbols, find approximate answers by + describing the underlying process as a combination of steps, each of + which can be assigned a number via symbolic manipulation". + For example, trying to mathematically compute how much water fits in + a completely crazy three dimensional shape is very hard, even if it + got you the perfect, precise answer. A much easier approach, which + would be less perfect but still entirely useful, would be to just + grab a buck and start filling the shape until it was full: just + count the number of buckets of water you used. And if we want a more + precise answer, we can use smaller buckets. +

+

+ So that's what we're going to do here, too: we're going to treat the + problem as a sequence of steps, and the smaller we can make each + step, the closer we'll get to that "perfect, precise" answer. And as + it turns out, there is a really nice numerical root-finding algorithm, called the Newton-Raphsonthat - Newton), which we can make use of. + Newton), which we can make use of. The Newton-Raphson approach + consists of taking our impossible-to-solve function + f(x), picking some intial value + x (literally any value will do), and calculating + f(x). We can think of that value as the "height" of the + function at x. If that height is zero, we're done, we + have found a root. If it isn't, we calculate the tangent line at + f(x) and calculate at which x value + its height is zero (which we've already seen is very easy). + That will give us a new x and we repeat the process + until we find a root.

- The Newton-Raphson approach consists of picking a value - t (any value will do), and getting the corresponding value - of the function at that t value. For normal functions, we - can treat that value as a height. If the height is zero, we're done, - we have found a root. If it's not, we take the tangent of the curve - at that point, and extend it until it passes the x-axis, which will - be at some new point t. We then repeat the procedure with - this new value, and we keep doing this until we find our root. -

-

- Mathematically, this means that for some t, at step - n=1, we perform the following calculation until - fy(t) is zero, so that the next t is the same as - the one we already have: + Mathematically, this means that for some x, at step + n=1, we perform the following calculation until + fy(x) is zero, so that the next + t is the same as the one we already have:

- (The Wikipedia article has a decent animation for this process, so - I'm not adding a sketch for that here unless there are requests for - it) + (The Wikipedia article has a decent animation for this process, so I + will not add a graphic for that here)

Now, this works well only if we can pick good starting points, and - our curve is continuously differentiable and doesn't have - oscillations. Glossing over the exact meaning of those terms, the - curves we're dealing with conform to those constraints, so as long - as we pick good starting points, this will work. So the question is: - which starting points do we pick? + our curve is + continuously differentiable + and doesn't have + oscillations. Glossing over the exact meaning of those terms, the curves we're + dealing with conform to those constraints, so as long as we pick + good starting points, this will work. So the question is: which + starting points do we pick?

- As it turns out, Newton-Raphson is so blindingly fast, so we could + As it turns out, Newton-Raphson is so blindingly fast that we could get away with just not picking: we simply run the algorithm from t=0 to t=1 at small steps (say, 1/200th) and the result will be all the roots we want. Of course, this may pose problems for high order Bézier curves: 200 steps for a 200th order Bézier curve is going to go - wrong, but that's okay: there is no reason, ever, to use Bézier - curves of crazy high orders. You might use a fifth order curve to - get the "nicest still remotely workable" approximation of a full - circle with a single Bézier curve, that's pretty much as high as - you'll ever need to go. + wrong, but that's okay: there is no reason (at least, none that I + know of) to ever use Bézier curves of crazy high orders. + You might use a fifth order curve to get the "nicest still remotely + workable" approximation of a full circle with a single Bézier curve, + but that's pretty much as high as you'll ever need to go.

In conclusion:

@@ -2908,7 +2970,7 @@ function getCubicRoots(pa, pb, pc, pd) { Scripts are disabled. Showing fallback image. @@ -3065,16 +3127,38 @@ function getCubicRoots(pa, pb, pc, pd) { aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae:

- - + + + Scripts are disabled. Showing fallback image. + + + width="275" + height="275" + src="./chapters/aligning/cubic.js" + > + + + Scripts are disabled. Showing fallback image. +

Tight boxes