From 34fcf4e2de81376a756df453644d318a84e661c2 Mon Sep 17 00:00:00 2001 From: Pomax Date: Fri, 24 Feb 2017 08:59:10 -0800 Subject: [PATCH] no need to put intermediary files on github --- locales/en-GB/content.js | 1901 ------------------------------------- locales/ja-JP/content.js | 1902 -------------------------------------- locales/zh-CN/content.js | 1902 -------------------------------------- 3 files changed, 5705 deletions(-) delete mode 100644 locales/en-GB/content.js delete mode 100644 locales/ja-JP/content.js delete mode 100644 locales/zh-CN/content.js diff --git a/locales/en-GB/content.js b/locales/en-GB/content.js deleted file mode 100644 index e003a9fa..00000000 --- a/locales/en-GB/content.js +++ /dev/null @@ -1,1901 +0,0 @@ -var React = require('react'); -var Graphic = require("../../components/Graphic.jsx"); -var SectionHeader = require("../../components/SectionHeader.jsx"); - -module.exports = { - "preface": { - "locale": "en-GB", - "title": "Preface", - "getContent": function(handler) { return
- -

In order to draw things in 2D, we usually rely on lines, which typically get classified into two categories: straight lines, and curves. The first of these are as easy to draw as they are easy to make a computer draw. Give a computer the first and last point in the line, and BAM! straight line. No questions asked.

-

Curves, however, are a much bigger problem. While we can draw curves with ridiculous ease freehand, computers are a bit handicapped in that they can't draw curves unless there is a mathematical function that describes how it should be drawn. In fact, they even need this for straight lines, but the function is ridiculously easy, so we tend to ignore that as far as computers are concerned, all lines are "functions", regardless of whether they're straight or curves. However, that does mean that we need to come up with fast-to-compute functions that lead to nice looking curves on a computer. There's a number of these, and in this article we'll focus on a particular function that has received quite a bit of attention, and is used in pretty much anything that can draw curves: "Bézier" curves

-

They're named after Pierre Bézier, who is principally responsible for getting them known to the world as a curve well-suited for design work (working for Renault and publishing his investigations in 1962), although he was not the first, or only one, to "invent" these type of curves. One might be tempted to say that the mathematician Paul de Casteljau was first, investigating the nature of these curves in 1959 while working at Citroën, coming up with a really elegant way of figuring out how to draw them. However, de Casteljau did not publish his work, making the question "who was first" hard to answer in any absolute sense. Or is it? Bézier curves are, at their core, "Bernstein polynomials", a family of mathematical functions investigated by Sergei Natanovich Bernstein, with publications on them at least as far back as 1912. Anyway, that's mostly trivia, what you are more likely to care about is that these curves are handy: you can link up multiple Bézier curves so that the combination looks like a single curve. If you've ever drawn Photoshop "paths" or worked with vector drawing programs like Flash, Illustrator or nkscape, those curves you've been drawing are Bézier curves.

-

So, what if you need to program them yourself? What are the pitfalls? How do you draw them? What are the bounding boxes, how do you determine intersections, how can you extrude a curve, in short: how do you do everything that you might want when you do with these curves? That's what this page is for. Prepare to be mathed!

-

—Pomax (or in the tweetworld, @TheRealPomax)

- -
-

Note: virtually all Bézier graphics are interactive.

-

This page uses interactive examples, relying heavily on Bezier.js, as well as "real" maths (in LaTeX form) which is typeset using the most excellent MathJax library. The page is generated offline as a React application, using Webpack, which has made adding "view source" options considerably more challenging. I'm still trying to figure out how to add them back in, but it didn't feel like it should hold up deploying this update compared to the previous years' version.

-

This book is open source.

-

This book is an open source software project, and lives on two github repositorites. The first is https://github.com/pomax/bezierinfo and is the purely-for-presentation version you are viewing right now. The other repository is https://github.com/pomax/BezierInfo-2, which is the development version, housing all the html, javascript, and css. You can fork either of these, and pretty much do with them as you please, except for passing it off as your own work wholesale, of course =)

-

How complicated is the maths going to be?

-

Most of the mathematics in this Primer are early high school maths. If you understand basic arithmetic, and you know how to read English, you should be able to get by just fine. There will at times be far more complicated maths, but if you don't feel like digesting them, you can safely skip over them by either skipping over the "detail boxes" in section or by just jumping to the end of a section with maths that looks too involving. The end of sections typically simply list the conclusions so you can just work with those values directly.

-

Questions, comments:

-

If you have suggestions for new sections, hit up the Github issue tracker (also reachable from the repo linked to in the upper right). If you have questions about the material, there's currently no comment section while I'm doing the rewrite, but you can use the issue tracker for that as well. Once the rewrite is done, I'll add a general comment section back in, and maybe a more topical "select this section of text and hit the 'question' button to ask a question about it" system. We'll see.

-

Buy me a coffee?

-

If you enjoyed this book, or you simply found it useful for something you were trying to get done, and you were wondering how to let me know you appreciated this book, you can always buy me a coffee, however-much a coffee is where you live. This work has grown over the years, from a small primer to a 70ish print-page-equivalent reader on the subject of Bézier curves, and a lot of coffee went into the making of it. I don't regret a minute I spent on writing it, but I can always do with some more coffee to keep on writing!

-
-
; } - - }, - "introduction": { - "locale": "en-GB", - "title": "A lightning introduction", - "getContent": function(handler) { return
- -

Let's start with the good stuff: when we're talking about Bézier curves, we're talking about the things that you can see in the following graphics. They run from some start point to some end point, with their curvature influenced by one or more "intermediate" control points. Now, because all the graphics on this page are interactive, go manipulate those curves a bit: click-drag the points, and see how their shape changes based on what you do.

- -
- - -
-

These curves are used a lot in computer aided design and computer aided manufacturing (CAD/CAM) applications, as well as in graphic design programs like Adobe Illustrator and Photoshop, Inkscape, the Gimp, etc. and in graphic technologies like scalable vector graphics (SVG) and OpenType fonts (ttf/otf). A lot of things use Bézier curves, so if you want to learn more about them... prepare to get your learn on!

-
; } - - }, - "whatis": { - "locale": "en-GB", - "title": "So what makes a Bézier Curve?", - "getContent": function(handler) { return
- -

Playing with the points for curves may have given you a feel for how Bézier curves behave, but what are Bézier curves, really? There are two ways to explain what a Bézier curve is, and they turn out to be the entirely equivalent, but one of them uses complicated maths, and the other uses really simple maths. So... let's start with the simple explanation:

-

Bezier curves are the result of linear interpolations. That sounds complicated but you've been doing linear interpolation since you were very young: any time you had to point at something between two other things, you've been applying linear interpolation. It's simply "picking a point between two points".

-

If we know the distance between those two points, and we want a new point that is, say, 20% the distance away from the first point (and thus 80% the distance away from the second point) then we can compute that really easily:

-\[ -Given \left ( - \begin{align} - p_1 &= some\ point \\ - p_2 &= some\ other\ point \\ - distance &= (p_2 - p_1) \\ - ratio &= \frac{percentage}{100} \\ - \end{align} -\right ),\ our\ new\ point = p_1 + distance \cdot ratio -\]

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

- -

And that brings us to the complicated maths: calculus.

-

While it doesn't look like that's what we've just done, we actually just drew a quadratic curve, in steps, rather than in a single go. One of the fascinating parts about Bézier curves is that they can both be described in terms of polynomial functions, as well as in terms of very simple interpolations of interpolations of [...]. That, in turn, means we can look at what these curves can do based on both "real maths" (by examining the functions, their derivatives, and all that stuff), as well as by looking at the "mechanical" composition (which tells us that a curve will never extend beyond the points we used to construct it, for instance)

-

So let's start looking at Bézier curves a bit more in depth. Their mathematical expressions, the properties we can derive from those, and the various things we can do to, and with, Bézier curves.

-
; } - - }, - "explanation": { - "locale": "en-GB", - "title": "The mathematics of Bézier curves", - "getContent": function(handler) { return
- -

Bézier curves are a form of "parametric" function. Mathematically speaking, parametric functions are cheats: a "function" is actually a well defined term representing a mapping from any number of inputs to a single output. Numbers go in, a single number comes out. Change the numbers that go in, and the number that comes out is still a single number. Parametric functions cheat. They basically say "alright, well, we want multiple values coming out, so we'll just use more than one function". An illustration: Let's say we have a function that maps some value, let's call it x, to some other value, using some kind of number manipulation:

-\[ - f(x) = \cos(x) -\]

The notation f(x) is the standard way to show that it's a function (by convention called f if we're only listing one) and its output changes based on one variable (in this case, x). Change x, and the output for f(x) changes.

-

So far so good. Now, let's look at parametric functions, and how they cheat. Let's take the following two functions:

-\[ -\begin{matrix} - f(a) = \cos(a) \\ - f(b) = \sin(b) -\end{matrix} -\]

There's nothing really remarkable about them, they're just a sine and cosine function, but you'll notice the inputs have different names. If we change the value for a, we're not going to change the output value for f(b), since a isn't used in that function. Parametric functions cheat by changing that. In a parametric function all the different functions share a variable, like this:

-\[ -\left \{ \begin{matrix} - f_a(t) = \cos(t) \\ - f_b(t) = \sin(t) -\end{matrix} \right. -\]

Multiple functions, but only one variable. If we change the value for t, we change the outcome of both fa(t) and fb(t). You might wonder how that's useful, and the answer is actually pretty simple: if we change the labels fa(t) and fb(t) with what we usually mean with them for parametric curves, things might be a lot more obvious:

-\[ -\left \{ \begin{matrix} - x = \cos(t) \\ - y = \sin(t) -\end{matrix} \right. -\]

There we go. x/y coordinates, linked through some mystery value t.

-

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 (use your up and down arrow keys to change the plot end value):

- -

Bézier curves are (one in many classes of) parametric functions, and are characterised by using the same base function for all its dimensions. Unlike the above example, where the x and y values use different functions (one uses a sine, the other a cosine), Bézier curves use the "binomial polynomial" for both x and y. So what are binomial polynomials?

-

You may remember polynomials from high school, where they're those sums that look like:

-\[ - f(x) = a \cdot x^3 + b \cdot x^2 + c \cdot x + d -\]

If they have a highest order term they're called "cubic" polynomials, if it's it's a "square" polynomial, if it's just x it's a line (and if there aren't even any terms with x it's not a polynomial!)

-

Bézier curves are polynomials of t, rather than x, with the value for t fixed being between 0 and 1, with coefficients a, b etc. taking the "binomial" form, which sounds fancy but is actually a pretty simple description for mixing values:

-\[ -\begin{align*} - linear &= (1-t) + t \\ - square &= (1-t)^2 + 2 \cdot (1-t) \cdot t + t^2 \\ - cubic &= (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3 -\end{align*} -\]

I know what you're thinking: that doesn't look too simple, but if we remove t and add in "times one", things suddenly look pretty easy. Check out these binomial terms:

-\[ -\begin{align*} - linear &= \hskip{2.5em} 1 + 1 \\ - square &= \hskip{1.7em} 1 + 2 + 1\\ - cubic &= \hskip{0.85em} 1 + 3 + 3 + 1\\ - hypercubic &= 1 + 4 + 6 + 4 + 1 -\end{align*} -\]

Notice that 2 is the same as 1+1, and 3 is 2+1 and 1+2, and 6 is 3+3... As you can see, each time we go up a dimension, we simply start and end with 1, and everything in between is just "the two numbers above it, added together". Now that's easy to remember.

-

There's an equally simple way to figure out how the polynomial terms work: if we rename (1-t) to a and t to b, and remove the weights for a moment, we get this:

-\[ -\begin{align*} - linear &= BLUE[a] + RED[b] \\ - square &= BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot RED[b] + RED[b] \cdot RED[b] \\ - cubic &= BLUE[a] \cdot BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot BLUE[a] \cdot RED[b] + BLUE[a] \cdot RED[b] \cdot RED[b] + RED[b] \cdot RED[b] \cdot RED[b]\\ -\end{align*} -\]

It's basically just a sum of "every combination of a and b", progressively replacing a's with b's after every + sign. So that's actually pretty simple too. So now you know binomial polynomials, and just for completeness I'm going to show you the generic function for this:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} -\]

And that's the full description for Bézier curves. Σ in this function indicates that this is a series of additions (using the variable listed below the Σ, starting at ...=<value> and ending at the value listed on top of the Σ).

- -
-

How to implement the basis function

-

We could naively implement the basis function as a mathematical construct, using the function as our guide, like this:

-
function Bezier(n,t):
-  sum = 0
-  for(k=0; k<n; k++):
-    sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

I say we could, because we're not going to: the factorial function is incredibly expensive. And, as we can see from the above explanation, we can actually create Pascal's triangle quite easily without it: just start at [1], then [1,1], then [1,2,1], then [1,3,3,1], and so on, with each next row fitting 1 more number than the previous row, starting and ending with "1", with all the numbers in between being the sum of the previous row's elements on either side "above" the one we're computing.

-

We can generate this as a list of lists lightning fast, and then never have to compute the binomial terms because we have a lookup table:

-
lut = [      [1],           // n=0
-            [1,1],          // n=1
-           [1,2,1],         // n=2
-          [1,3,3,1],        // n=3
-         [1,4,6,4,1],       // n=4
-        [1,5,10,10,5,1],    // n=5
-       [1,6,15,20,15,6,1]]  // n=6
-
-binomial(n,k):
-  while(n >= lut.length):
-    s = lut.length
-    nextRow = new array(size=s+1)
-    nextRow[0] = 1
-    for(i=1, prev=s-1; i<prev; i++):
-      nextRow[i] = lut[prev][i-1] + lut[prev][i]
-    nextRow[s] = 1
-    lut.add(nextRow)
-  return lut[n][k]
-
-

So what's going on here? First, we declare a lookup table with a size that's reasonably large enough to accommodate most lookups. Then, we declare a function to get us the values we need, and we make sure that if an n/k pair is requested that isn't in the LUT yet, we expand it first. Our basis function now looks like this:

-
function Bezier(n,t):
-  sum = 0
-  for(k=0; k<=n; k++):
-    sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

Perfect. Of course, we can optimize further. For most computer graphics purposes, we don't need arbitrary curves. We need quadratic and cubic curves (this primer actually does do arbitrary curves, so you'll find code similar to shown here), which means we can drastically simplify the code:

-
function Bezier(2,t):
-  t2 = t * t
-  mt = 1-t
-  mt2 = mt * mt
-  return mt2 + 2*mt*t + t2
-
-function Bezier(3,t):
-  t2 = t * t
-  t3 = t2 * t
-  mt = 1-t
-  mt2 = mt * mt
-  mt3 = mt2 * mt
-  return mt3 + 3*mt2*t + 3*mt*t2 + t3
-
-

And now we know how to program the basis function. Exellent.

-
-

So, now we know what the base function(s) look(s) like, time to add in the magic that makes Bézier curves so special: control points.

-
; } - - }, - "control": { - "locale": "en-GB", - "title": "Controlling Bézier curvatures", - "getContent": function(handler) { return
- -

Bézier curves are (like all "splines") interpolation functions, meaning they take a set of points, and generate values somewhere "between" those points. (One of the consequences of this is that you'll never be able to generate a point that lies outside the outline for the control points, commonly called the "hull" for the curve. Useful information!). In fact, we can visualize how each point contributes to the value generated by the function, so we can see which points are important, where, in the curve.

-

The following graphs show the interpolation functions for quadratic and cubic curves, with "S" being the strength of a point's contribution to the total sum of the Bézier function. Click or click-drag to see the interpolation percentages for each curve-defining point at a specific t value.

- -
- - - -
-

Also shown is the interpolation function for a 15th order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.

-

If we want to change the curve, we need to change the weights of each point, effectively changing the interpolations. The way to do this is about as straight forward as possible: just multiply each point with a value that changes its strength. These values are conventionally called "Weights", and we can add them to our original Bézier function:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} - \cdot\ - \underset{weight}{\underbrace{w_i}} -\]

That looks complicated, but as it so happens, the "weights" are actually just the coordinate values we want our curve to have: for an nth - order curve, w0 is our start coordinate, wn is our last coordinate, and everything in between is a controlling coordinate. Say we want a cubic curve that starts at (120,160), is controlled by (35,200) and (220,260) and ends at (220,40), we use this Bézier curve:

-\[ -\left \{ \begin{matrix} - x = BLUE[120] \cdot (1-t)^3 + BLUE[35] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[220] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[220] \cdot t^3 \\ - y = BLUE[160] \cdot (1-t)^3 + BLUE[200] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[260] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[40] \cdot t^3 -\end{matrix} \right. -\]

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

- -

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.

- -
-

How to implement the weighted basis function

-

Given that we already know how to implement basis function, adding in the control points is remarkably easy:

-
function Bezier(n,t,w[]):
-  sum = 0
-  for(k=0; k<n; k++):
-    sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

And for the extremely optimized versions:

-
function Bezier(2,t,w[]):
-  t2 = t * t
-  mt = 1-t
-  mt2 = mt * mt
-  return w[0]*mt2 + w[1]*2*mt*t + w[2]*t2
-
-function Bezier(3,t,w[]):
-  t2 = t * t
-  t3 = t2 * t
-  mt = 1-t
-  mt2 = mt * mt
-  mt3 = mt2 * mt
-  return w[0]*mt3 + 3*w[1]*mt2*t + 3*w[2]*mt*t2 + w[3]*t3
-
-

And now we know how to program the weighted basis function.

-
-
; } - - }, - "extended": { - "locale": "en-GB", - "title": "The Bézier interval [0,1]", - "getContent": function(handler) { return
- -

Now that we know the mathematics behind Bézier curves, there's one curious thing that you may have noticed: they always run from t=0 to t=1. Why that particular interval?

-

It all has to do with how we run from "the start" of our curve to "the end" of our curve. If we have a value that is a mixture of two other values, then the general formula for this is:

-\[ - mixture = a \cdot value_1 + b \cdot value_2 -\]

The obvious start and end values here need to be a=1, b=0, so that the mixed value is 100% value 1, and 0% value 2, and a=0, b=1, so that the mixed value is 0% value 1 and 100% value 2. Additionally, we don't want "a" and "b" to be independent: if they are, then we could just pick whatever values we like, and end up with a mixed value that is, for example, 100% value 1 and 100% value 2. In principle that's fine, but for Bézier curves we always want mixed values between the start and end point, so we need to make sure we can never set "a" and "b" to some values that lead to a mix value that sums to more than 100%. And that's easy:

-\[ - m = a \cdot value_1 + (1 - a) \cdot value_2 -\]

With this we can guarantee that we never sum above 100%. By restricting a to values in the interval [0,1], we will always be somewhere between our two values (inclusively), and we will always sum to a 100% mix.

-

But... what if we use this form, used in the assumption that we will only ever use values between 0 and 1, and instead use values outside of that interval? Do things go horribly wrong? Well... not really, but we get to "see more".

-

In the case of Bézier curves, extending the interval simply makes our curve "keep going". Bézier curves are simply segments on some polynomial curve, so if we pick a wider interval we simply get to see more of the curve. So what do they look like?

-

The following two graphics show you Bézier curves rendered "the usual way", as well as the curves they "lie on" if we were to extend the t values much further. As you can see, there's a lot more "shape" hidden in the rest of the curve, and we can model those parts by moving the curve points around.

- - -

In fact, there are curves used in graphics design and computer modelling that do the opposite of Bézier curves, where rather than fixing the interval, and giving you free coordinates, they fix the coordinates, but give you freedom over the interval. A great example of this is the "Spiro" curve, which is a curve based on part of a Cornu Spiral, also known as Euler's Spiral. It's a very aesthetically pleasing curve and you'll find it in quite a few graphics packages like FontForge and Inkscape, having even been used in font design (such as for the Inconsolata font).

-
; } - - }, - "matrix": { - "locale": "en-GB", - "title": "Bézier curvatures as matrix operations", - "getContent": function(handler) { return
- -

We can also represent Bézier as matrix operations, by expressing the Bézier formula as a polynomial basis function, the weight matrix, and the actual coordinates as matrix. Let's look at what this means for the cubic curve:

-\[ - B(t) = P_1 \cdot (1-t)^3 + P_2 \cdot 3 \cdot (1-t)^2 \cdot t + P_3 \cdot 3 \cdot (1-t) \cdot t^2 + P_4 \cdot t^3 -\]

Disregarding our actual coordinates for a moment, we have:

-\[ - B(t) = (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3 -\]

We can write this as a sum of four expressions:

-\[ - \begin{matrix} - ... & = & (1-t)^3 \\ - & + & 3 \cdot (1-t)^2 \cdot t \\ - & + & 3 \cdot (1-t) \cdot t^2 \\ - & + & t^3 \\ - \end{matrix} -\]

And we can expand these expressions:

-\[ - \begin{matrix} - ... & = & (1-t) \cdot (1-t) \cdot (1-t) & = & -t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\ - & + & 3 \cdot (1-t) \cdot (1-t) \cdot t & = & 3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t \\ - & + & 3 \cdot (1-t) \cdot t \cdot t & = & -3 \cdot t^3 + 3 \cdot t^2 \\ - & + & t \cdot t \cdot t & = & t^3 \\ - \end{matrix} -\]

Furthermore, we can make all the 1 and 0 factors explicit:

-\[ - \begin{matrix} - ... & = & -1 \cdot t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\ - & + & +3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t + 0 \\ - & + & -3 \cdot t^3 + 3 \cdot t^2 + 0 \cdot t + 0 \\ - & + & +1 \cdot t^3 + 0 \cdot t^2 + 0 \cdot t + 0 \\ - \end{matrix} -\]

And that, we can view as a series of four matrix operations:

-\[ - \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-1 \\ 3 \\ -3 \\ 1\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}3 \\ -6 \\ 3 \\ 0\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-3 \\ 3 \\ 0 \\ 0\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}1 \\ 0 \\ 0 \\ 0\end{bmatrix} -\]

If we compact this into a single matrix operation, we get:

-\[ - \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix} - -1 & 3 & -3 & 1 \\ - 3 & -6 & 3 & 0 \\ - -3 & 3 & 0 & 0 \\ - 1 & 0 & 0 & 0 - \end{bmatrix} -\]

This kind of polynomial basis representation is generally written with the bases in increasing order, which means we need to flip our t matrix horizontally, and our big "mixing" matrix upside down:

-\[ - \begin{bmatrix}1 & t & t^2 & t^3\end{bmatrix} \cdot \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} -\]

And then finally, we can add in our original coordinates as a single third matrix:

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

We can perform the same trick for the quadratic curve, in which case we end up with:

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

If we plug in a t value, and then multiply the matrices, we will get exactly the same values as when we evaluate the original polynomial function, or as when we evaluate the curve using progessive linear interpolation.

-

-So: why would we bother with matrices? Matrix representations allow us to discover things about functions that would otherwise be hard to tell. It turns out that the curves form triangular matrices, and they have a determinant equal to the product of the actual coordinates we use for our curve. It's also invertible, which means there's a ton of properties that are all satisfied. Of course, the main question is: "Why is this useful to us, now?", and the answer to that is that it's not immediately useful, but you'll be seeing some instances where certain curve properties can be either computed via function manipulation, or via clever use of matrices, and sometimes the matrix approach can be (drastically) faster.

-

So for now, just remember that we can represent curves this way, and let's move on.

-
; } - - }, - "decasteljau": { - "locale": "en-GB", - "title": "de Casteljau's algorithm", - "getContent": function(handler) { return
- -

If we want to draw Bézier curves we can run through all values of t from 0 to 1 and then compute the weighted basis function, getting the x/y values we need to plot, but the more complex the curve gets, the more expensive this becomes. Instead, we can use "de Casteljau's algorithm" to draw curves, which is a geometric approach to drawing curves, and really easy to implement. So easy, in fact, you can do it by hand with a pencil and ruler.

-

Rather than using our calculus function to find x/y values for t, let's do this instead:

-
    -
  • treat t as a ratio (which it is). t=0 is 0% along a line, t=1 is 100% along a line.
  • -
  • Take all lines between the curve's defining points. For an order n curve, that's n lines.
  • -
  • Place markers along each of these line, at distance t. So if t is 0.2, place the mark at 20% from the start, 80% from the end.
  • -
  • Now form lines between those points. This gives n-1 lines.
  • -
  • Place markers along each of these line at distance t.
  • -
  • Form lines between those points. This'll be n-2 lines.
  • -
  • place markers, form lines, place markers, etc.
  • -
  • repeat this until you have only one line left. The point t on that line coincides with the original curve point at t.
  • -
- -
-

How to implement de Casteljau's algorithm

-

Let's just use the algorithm we just specified, and implement that:

-
function drawCurve(points[], t):
-  if(points.length==1):
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      newpoints[i] = (1-t) * points[i] + t * points[i+1]
-    drawCurve(newpoints, t)
-
-

And done, that's the algorithm implemented. Except usually you don't get the luxury of overloading the "+" operator, so let's also give the code for when you need to work with x and y values:

-
function drawCurve(points[], t):
-  if(points.length==1):
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      x = (1-t) * points[i].x + t * points[i+1].x
-      y = (1-t) * points[i].y + t * points[i+1].y
-      newpoints[i] = new point(x,y)
-    drawCurve(newpoints, t)
-
-

So what does this do? This draws a point, if the passed list of points is only 1 point long. Otherwise it will create a new list of points that sit at the t ratios (i.e. the "markers" outlined in the above algorithm), and then call the draw function for this new list.

-
-

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.

- -
; } - - }, - "flattening": { - "locale": "en-GB", - "title": "Simplified drawing", - "getContent": function(handler) { return
- -

We can also simplify the drawing process by "sampling" the curve at certain points, and then joining those points up with straight lines, a process known as "flattening", as we are reducing a curve to a simple sequence of straight, "flat" lines.

-

We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.

- - -

Try clicking on the sketch and using your up and down arrow keys to lower the number of segments for both the quadratic and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.

- -
-

How to implement curve flattening

-

Let's just use the algorithm we just specified, and implement that:

-
function flattenCurve(curve, segmentCount):
-  step = 1/segmentCount;
-  coordinates = [curve.getXValue(0), curve.getYValue(0)]
-  for(i=1; i <= segmentCount; i++):
-    t = i*step;
-    coordinates.push[curve.getXValue(t), curve.getYValue(t)]
-  return coordinates;
-
-

And done, that's the algorithm implemented. That just leaves drawing the resulting "curve" as a sequence of lines:

-
function drawFlattenedCurve(curve, segmentCount):
-  coordinates = flattenCurve(curve, segmentCount)
-  coord = coordinates[0], _coords;
-  for(i=1; i < coordinates.length; i++):
-    _coords = coordinates[i]
-    line(coords, _coords)
-    coords = _coords
-
-

We start with the first coordinate as reference point, and then just draw lines between each point and its next point.

-
-
; } - - }, - "splitting": { - "locale": "en-GB", - "title": "Splitting curves", - "getContent": function(handler) { return
- -

With de Casteljau's algorithm we 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.

- -
-

implementing curve splitting

-

We can implement curve splitting by bolting some extra logging onto the de Casteljau function:

-
left=[]
-right=[]
-function drawCurve(points[], t):
-  if(points.length==1):
-    left.add(points[0])
-    right.add(points[0])
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      if(i==0):
-        left.add(points[i])
-      if(i==newpoints.length-1):
-        right.add(points[i+1])
-      newpoints[i] = (1-t) * points[i] + t * points[i+1]
-    drawCurve(newpoints, t)
-
-

After running this function for some value t, the left and right arrays will contain all the coordinates for two new curves - one to the "left" of our t value, the other on the "right", of the same order as the original curve, and overlayed exactly on the original curve.

-
-

This is best illustrated with an animated graphic (click to play/pause):

- -
; } - - }, - "matrixsplit": { - "locale": "en-GB", - "title": "Splitting curves using matrices", - "getContent": function(handler) { return
- -

Another way to split curves is to exploit the matrix representation of a Bézier curve. In the section on matrices we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic, and cubic curves, respectively (using the reversed Bézier coefficients vector for legibility):

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

and

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0\\ - -3 & 3 & 0 & 0\\ - 3 & -6 & 3 & 0\\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

Let's say we want to split the curve at some point t = z, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we split out the the actual "point on the curve" information as a new matrix multiplication:

-\[ - B(t) = - \begin{bmatrix} - 1 & (z \cdot t) & (z \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

and

-\[ - B(t) = - \begin{bmatrix} - 1 & (z \cdot t) & (z \cdot t)^2 & (z \cdot t)^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0\\ - 0 & z & 0 & 0\\ - 0 & 0 & z^2 & 0\\ - 0 & 0 & 0 & z^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

If we could compact these matrices back to a form [t values] · [bezier matrix] · [column matrix], with the first two staying the same, then that column matrix on the right would be the coordinates of a new Bézier curve that describes the first segment, from t = 0 to t = z. As it turns out, we can do this quite easily, by exploiting some simple rules of linear algebra (and if you don't care about the derivations, just skip to the end of the box for the results!).

- -
-

Deriving new hull coordinates

-

Deriving the two segments upon splitting a curve takes a few steps, and the higher the curve order, the more work it is, so let's look at the quadratic curve first:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \underset{we\ turn\ this...}{\underbrace{\kern 2.25em Z \cdot M \kern 2.25em}} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \underset{...into\ this...}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \underset{...to\ get\ this!}{\underbrace{ \kern 1.25em \cdot \kern 1.25em Q \kern 1.25em \cdot \kern 1.25em}} - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

We do this, because [M · M-1 -] is the identity matrix (a bit like multiplying something by x/x in calculus. It doesn't do anything to the function, but it does allow you to rewrite it to something that may be easier to work with, or can be broken up differently). Adding that as matrix multiplication has no effect on the total formula, but it does allow us to change the matrix sequence [something · M] to a sequence [M · something], and that makes a world of difference: if we know what [M-1 · Z · M] is, we can apply that to our coordinates, and be left with a proper matrix representation of a quadratic Bézier curve (which is [T · M · P]), with a new set of coordinates that represent the curve from t = 0 to t = z. So let's get computing:

-\[ - Q = M^{-1} \cdot Z \cdot M = - \begin{bmatrix} - 1 & 0 & 0 \\ - 1 & \frac{1}{2} & 0 \\ - 1 & 1 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - = - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} -\]

Excellent! Now we can form our new quadratic curve:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot M \cdot Q \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \cdot - \left ( - Q - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \left ( - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1 - \end{bmatrix} -\]

- -Brilliant -: if we want a subcurve from t = 0 to t = z, we can keep the first coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the start point, and the new end point is a mixture that looks oddly similar to a bernstein polynomial of degree two, except it uses (z-1) rather than (1-z)... These new coordinates are actually really easy to compute directly!

-

Of course, that's only one of the two curves. Getting the section from t = z to t = 1 requires doing this again. We first observe what what we just did is actually evaluate the general interval [0,z], which we wrote down simplified becuase of that zero, but we actually evaluated this:

-\[ - B(t) = - \begin{bmatrix} - 1 & ( 0 + z \cdot t) & ( 0 + z \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

If we want the interval [z,1], we will be evaluating this instead:

-\[ - B(t) = - \begin{bmatrix} - 1 & ( z + (1-z) \cdot t) & ( z + (1-z) \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & z & z^2 \\ - 0 & 1-z & 2 \cdot z \cdot (1-z) \\ - 0 & 0 & (1-z)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

We're going to do the same trick, to turn [something · M] into [M · something]:

-\[ - Q' = M^{-1} \cdot Z' \cdot M = - \begin{bmatrix} - 1 & 0 & 0 \\ - 1 & \frac{1}{2} & 0 \\ - 1 & 1 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & z & z^2 \\ - 0 & 1-z & 2 \cdot z \cdot (1-z) \\ - 0 & 0 & (1-z)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - = - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} -\]

So, our final second curve looks like:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot M \cdot Q \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \cdot - \left ( - Q' - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \left ( - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z \cdot P_3 - (z-1) \cdot P_2 \\ - P_3 - \end{bmatrix} -\]

- -Nice -: we see the same as before; can keep the last coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the end point, and the new start point is a mixture that looks oddly similar to a bernstein polynomial of degree two, except it uses (z-1) rather than (1-z). These new coordinates are also really easy to compute directly!

-
-

So, using linear algebra rather than de Casteljau's algorithm, we have determined that for any quadratic curve split at some value t = z, we get two subcurves that are described as Bézier curves with simple-to-derive coordinates.

-\[ - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1 - \end{bmatrix} -\]

and

-\[ - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z \cdot P_3 - (z-1) \cdot P_2 \\ - P_3 - \end{bmatrix} -\]

We can do the same for cubic curves. However, I'll spare you the actual derivation (don't let that stop you from writing that out yourself, though) and simply show you the resulting new coordinate sets:

-\[ - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -(z-1) & z & 0 & 0 \\ - (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 & 0 \\ - -(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 - \end{bmatrix} -\]

and

-\[ - \begin{bmatrix} - -(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1)^3 \cdot z^2 & z^3 \\ - 0 & (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 \\ - 0 & 0 & -(z-1) & z \\ - 0 & 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \\ - z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \\ - z \cdot P_4 - (z-1) \cdot P_3 \\ - P_4 - \end{bmatrix} -\]

So, looking at our matrices, did we really need to compute the second segment matrix? No, we didn't. Actually having one segment's matrix means we implicitly have the other: push the values of each row in the matrix -Q - to the right, with zeroes getting pushed off the right edge and appearing back on the left, and then flip the matrix vertically. Presto, you just "calculated" -Q' -.

-

Implementing curve splitting this way requires less recursion, and is just straight arithmetic with cached values, so can be cheaper on systems were recursion is expensive. If you're doing computation with devices that are good at matrix multiplication, chopping up a Bézier curve with this method will be a lot faster than applying de Casteljau.

-
; } - - }, - "reordering": { - "locale": "en-GB", - "title": "Lowering and elevating curve order", - "getContent": function(handler) { return
- -

One interesting property of Bézier curves is that an nth - order curve can always be perfectly represented by an (n+1)th - order curve, by giving the higher order curve specific control points.

-

If we have a curve with three points, then we can create a four point curve that exactly reproduce the original curve as long as we give it the same start and end points, and for its two control points we pick "1/3rd start + 2/3rd control" and "2/3rd control + 1/3rd end", and now we have exactly the same curve as before, except represented as a cubic curve, rather than a quadratic curve.

-

The general rule for raising an nth - order curve to an (n+1)th - order curve is as follows (observing that the start and end weights are the same as the start and end weights for the old curve):

-\[ - Bézier(k,t) = \sum_{i=0}^{k} - \underset{binomial\ term}{\underbrace{\binom{k}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}} - \ \cdot \ - \underset{new\ weights}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}} - \ ,\ with\ k = n+1\ and\ w_{i-1}=0\ when\ i = 0 -\]

However, this rule also has as direct consequence that you cannot generally safely lower a curve from nth - order to (n-1)th - order, because the control points cannot be "pulled apart" cleanly. We can try to, but the resulting curve will not be identical to the original, and may in fact look completely different.

-

We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch and press your up and down arrow keys to elevate or lower the curve order.

- -

There is a good, if mathematical, explanation on the matrices necessary for optimal reduction over on Sirver's Castle, which given time will find its way in a more direct description into this article.

-
; } - - }, - "derivatives": { - "locale": "en-GB", - "title": "Derivatives", - "getContent": function(handler) { return
- -

There's a number of useful things that you can do with Bézier curves based on their derivative, and one of the more amusing observations about Bézier curves is that their derivatives are, in fact, also Bézier curves. In fact, the derivation of a Bézier curve is relatively straight forward, although we do need a bit of math. First, let's look at the derivative rule for Bézier curves, which is:

-\[ - Bézier'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot Bézier(n-1,t)_i -\]

which we can also write (observing that b in this formula is the same as our w weights, and that n times a summation is the same as a summation where each term is multiplied by n) as:

-\[ - Bézier'(n,t) = \sum_{i=0}^{n-1} Bézier(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i) -\]

Or, in plain text: the derivative of an nth degree Bézier curve is an (n-1)th degree Bézier curve, with one fewer term, and new weights w'0...w'n-1 derived from the original weights as n(wi+1 - wi), so for a 3rd degree curve, with four weights, the derivative has three new weights w'0 = 3(w1-w0), w'1 = 3(w2-w1) and w'2 = 3(w3-w2).

- -
-

"Slow down, why is that true?"

-

Sometimes just being told "this is the derivative" is nice, but you might want to see why this is indeed the case. As such, let's have a look at the proof for this derivative. First off, the weights are independent of the full Bézier function, so the derivative involves only the derivative of the polynomial basis function. So, let's find that:

-\[ - B_{n,k}(t) \frac{d}{dt} = {n \choose k} t^k (1-t)^{n-k} \frac{d}{dt} -\]

Applying the product and chain rules gives us:

-\[\begin{array}{l} - ... &= {n \choose k} \left ( - k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1 - \right ) -\end{array}\]

Which is hard to work with, so let's expand that properly:

-\[\begin{array}{l} - ... &= \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} -\end{array}\]

Now, the trick is to turn this expression into something that has binomial coefficients again, so we want to end up with things that look like "x! over y!(x-y)!". If we can do that in a way that involves terms of n-1 and k-1, we'll be on the right track.

-\[\begin{array}{l} - ... &= \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\ - - ... &= n \left ( - \frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k} - \right ) \\ - - ... &= n \left ( - \frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{(k-1)} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k} - \right ) -\end{array}\]

And that's the first part done: the two components inside the parentheses are actually regular, lower order Bezier expressions:

-\[\begin{array}{l} - ... &= n \left ( - \frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k} - \right ) - \ ,\ with\ x=n-1,\ y=k-1 - \\ - - - ... &= n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right ) -\end{array}\]

Now to apply this to our weighted Bezier curves. We'll write out the plain curve formula that we saw earlier, and then work our way through to its derivative:

-\[\begin{array}{l} - Bézier_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + ... \\ - Bézier_{n,k}(t) \frac{d}{dt} &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + \\ - & & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + \\ - & & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + \\ - & & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + \\ - & & ... -\end{array}\]

If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for k we see the following:

-\[\begin{array}{l} - n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\ - n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\ - n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\ - n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\ - ... &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\ - ... & & & -\end{array}\]

Two of these terms fall way: the first term falls away because there is no -1st term in a summation. As such, it always contributes "nothing", so we can safely completely ignore it for the purpose of finding the derivative function. The other term is the very last term in this expansion: one involving Bn-1,n -. This term would have a binomial coefficient of [i choose i+1], which is a non-existent binomial coefficient. Again, this term would contribute "nothing", so we can ignore it, too. This means we're left with:

-\[\begin{array}{l} - n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\ - n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& \ n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\ - n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\ - ... -\end{array}\]

And that's just a summation of lower order curves:

-\[ - Bézier_{n,k}(t) \frac{d}{dt} = n \cdot B_{(n-1),BLUE[0]}(t) \cdot (w_1 - w_0) - + n \cdot B_{(n-1),RED[1]}(t) \cdot (w_2 - w_1) - + n \cdot B_{(n-1),MAGENTA[2]}(t) \cdot (w_3 - w_2) - \ + \ ... -\]

We can rewrite this as a normal summation, and we're done:

-\[ - Bézier_{n,k}(t) \frac{d}{dt} = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k) - = \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underset{derivative\ weights} - {\underbrace{n \cdot (w_{k+1} - w_k)}} -\]
-

Let's rewrite that in a form similar to our original formula, so we can see the difference. We will first list our original formula for Bézier curves, and then the derivative:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} - \cdot\ - \underset{weight}{\underbrace{w_i}} -\]\[ - Bézier'(n,t) = \sum_{i=0}^{k} - \underset{binomial\ term}{\underbrace{\binom{k}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}} - \cdot\ - \underset{derivative\ weight}{\underbrace{n \cdot (w_{i+1} - w_i)}} - {\ , \ with \ k=n-1} -\]

What are the differences? In terms of the actual Bézier curve, virtually nothing! We lowered the order (rather than n, it's now n-1), but it's still the same Bézier function. The only real difference is in how the weights change when we derive the curve's function. If we have four points A, B, C, and D, then the derivative will have three points, the second derivative two, and the third derivative one:

-\[ \begin{array}{l} - B(n,t), & & w = \{A,B,C,D\} \\ - B'(n,t), & n = 3, & w' = \{A',B',C'\} &= \{3 \cdot (B-A), {\ } 3 \cdot (C-B), {\ } 3 \cdot (D-C)\} \\ - B''(n,t), & n = 2, & w'' = \{A'',B''\} &= \{2 \cdot (B'-A'), {\ } 2 \cdot (C'-B')\} \\ - B'''(n,t), & n = 1, & w''' = \{A'''\} &= \{1 \cdot (B''-A'')\} -\end{array} \]

We can keep performing this trick for as long as we have more than one weight. Once we have one weight left, the next step will see k = 0, and the result of our "Bézier function" summation is zero, because we're not adding anything at all. As such, a quadratic curve has no second derivative, a cubic curve has no third derivative, and generalized: an nth - order curve has n-1 (meaningful) derivatives, with any further derivative being zero.

-
; } - - }, - "pointvectors": { - "locale": "en-GB", - "title": "Tangents and normals", - "getContent": function(handler) { return
- -

If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested in are the tangent vector and normal vector for curve points. These are actually really easy to find. For moving, and orienting, along a curve we use the tangent, which indicates the direction travel at specific points, and is literally just the first derivative of our curve:

-\[ -\left \{ \begin{matrix} - tangent_x(t) = B'_x(t) \\ - tangent_y(t) = B'_y(t) -\end{matrix} \right. -\]

This gives us the directional vector we want. We can normalize it to give us uniform directional vectors (having a length of 1.0) at each point, and then do whatever it is we want to do based on those directions:

-\[ - d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2} -\]\[ -\left \{ \begin{matrix} - \hat{x}(t) = || tangent_x(t) || - =\frac{tangent_x(t)}{ || tangent(t) || } - = \frac{B'_x(t)}{d} \\ - \hat{y}(t) = || tangent_y(t) || - = \frac{tangent_y(t)}{ || tangent(t) || } - = \frac{B'_y(t)}{d} -\end{matrix} \right. -\]

The tangent is very useful for moving along a line, but what if we want to move away from the curve instead, perpendicular to the curve at some point t? In that case we want the "normal" vector. This vector runs at a right angle to the direction of the curve, and is typically of length 1.0, so all we have to do is rotate the normalized directional vector and we're done:

-\[ -\left \{ \begin{array}{l} - normal_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\ - normal_y(t) = \underset{quarter\ circle\ rotation} {\underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }} = \hat{x}(t) -\end{array} \right. -\] -
-

Rotating coordinates is actually very easy, if you know the rule for it. You might find it explained as "applying a rotation matrix, which is what we'll look at here, too. Essentially, the idea is to take the circles over which we can rotate, and simply "sliding the coordinates" over these circles by the desired -angle. If we want a quarter circle turn, we take the coordinate, slide it along the cirle by a quarter turn, and done.

-

To turn any point (x,y) into a rotated point (x',y') (over 0,0) by some angle φ, we apply this nicely easy computation:

-\[\begin{array}{l} - x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\ - y' = x \cdot \sin(\phi) + y \cdot \cos(\phi) -\end{array}\]

Which is the "long" version of the following matrix transformation:

-\[ - \begin{bmatrix} - x' \\ y' - \end{bmatrix} - = - \begin{bmatrix} - \cos(\phi) & -\sin(\phi) \\ - \sin(\phi) & \cos(\phi) - \end{bmatrix} - \begin{bmatrix} - x \\ y - \end{bmatrix} -\]

And that's all we need to rotate any coordinate. Note that for quarter, half and three quarter turns these functions become even easier, since sin and cos for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.

-

But -why - does this work? Why this matrix multiplication? Wikipedia (Technically, Thomas Herter and Klaus Lott) tells us that a rotation matrix can be -treated as a sequence of three (elementary) shear operations. When we combine this into a single matrix operation (because all matrix multiplications can be collapsed), we get the matrix that you see above. DataGenetics have an excellent article about this very thing: it's really quite cool, and I strongly recommend taking a quick break from this primer to read that article.

-
-

The following two graphics show the tangent and normal along a quadratic and cubic curve, with the direction vector coloured blue, and the normal vector coloured red (the markers are spaced out evenly as t-intervals, not spaced equidistant).

- -
- - -
-
; } - - }, - "components": { - "locale": "en-GB", - "title": "Component functions", - "getContent": function(handler) { return
- -

One of the first things people run into when they start using Bézier curves in their own programs is "I know how to draw the curve, but how do I determine the bounding box?". It's actually reasonably straight forward to do so, but it requires having some knowledge on exploiting math to get the values we need. For bounding boxes, we aren't actually interested in the curve itself, but only in its "extremities": the minimum and maximum values the curve has for its x- and y-axis values. If you remember your calculus (provided you ever took calculus, otherwise it's going to be hard to remember) we can determine function extremities using the first derivative of that function, but this poses a problem, since our function is parametric: every axis has its own function.

-

The solution: compute the derivative for each axis separately, and then fit them back together in the same way we do for the original.

-

Let's look at how a parametric Bézier curve "splits up" into two normal functions, one for the x-axis and one for the y-axis. Note the left-most figure is again an interactive curve, without labeled axes (you get coordinates in the graph instead). The center and right-most figures are the component functions for computing the x-axis value, given a value for t (between 0 and 1 inclusive), and the y-axis value, respectively.

-

If you move points in a curve sideways, you should only see the middle graph change; likely, moving points vertically should only show a change in the right graph.

- - -
; } - - }, - "extremities": { - "locale": "en-GB", - "title": "Finding extremities: root finding", - "getContent": function(handler) { return
- -

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

-

Quadratic curves: linear derivatives.

-

Finding the solution for "where is this line 0" should be trivial:

-\[ -\begin{align} - l(x) = ax + b &= 0,\\ - ax + b &= 0,\\ - ax &= -b \\ - x &= \frac{-b}{a} -\end{align} -\]

Done. And quadratic curves have no meaningful second derivative, so we're really done.

-

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

-

First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the derivatives section:

-\[ -\begin{align} - B(t)\ uses\ \{ p_1,p_2,p_3,p_4 \} \\ - B'(t)\ uses\ \{ v_1.v_2,v_3 \},\ where\ v_1 = 3(p_2-p_1),\ v_2 = 3(p_3-p_2),\ v_3 = 3(p_4-p_3) -\end{align} -\]

And then, using these v values, we can find out what our a, b, and c should be:

-\[ -\begin{align} - B'(t) &= v_1(1-t)^2 + 2v_2(1-t)t + v_3t^2 \\ - ... &= v_1(t^2 - 2t + 1) + 2v_2(t-t^2) + v_3t^2 \\ - ... &= v_1t^2 - 2v_1t + v_1 + 2v_2t - 2v_2t^2 + v_3t^2 \\ - ... &= v_1t^2 - 2v_2t^2 + v_3t^2 - 2v_1t + v_1 + 2v_2t \\ - ... &= (v_1-2v_2+v_3)t^2 + 2(v_2-v_1)t + v_1 -\end{align} -\]

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:

-\[ -\begin{align} - a &= v_1-2v_2+v_3 = 3(-p_1 + 3p_2 - 3p_3 + p_4) \\ - b &= 2(v_2-v_1) = 6(p_1 - 2p_2 + p_3) \\ - c &= v_1 = 3(p_2-p_1) -\end{align} -\]

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.

-

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

-\[ - very\ hard:\ solve\ at^3 + bt^2 + ct + d = 0\\ - easier:\ solve\ t^3 + pt + q = 0 -\]

This is easier because for the "easier formula" we can 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).

-

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

- -
-

Implementing Cardano's algorithm for finding all real roots

-

The "real roots" part is fairly important, because while you cannot take a square, cube, etc. root of a negative number in the "real" number space (denoted with ℝ), this is perfectly fine in the "complex" number space (denoted with ℂ). And, as it so happens, Cardano is also attributed as the first mathematician in history to have made use of complex numbers in his calculations. For this very algorithm!

-
// A helper function to filter for values in the [0,1] interval:
-function accept(t) {
-  return 0<=t && t <=1;
-}
-
-// A real-cuberoots-only function:
-function crt(v) {
-  if(v<0) return -Math.pow(-v,1/3);
-  return Math.pow(v,1/3);
-}
-
-// Now then: given cubic coordinates {pa, pb, pc, pd} find all roots.
-function getCubicRoots(pa, pb, pc, pd) {
-  var d = (-pa + 3*pb - 3*pc + pd),
-  a = (3*pa - 6*pb + 3*pc) / d,
-  b = (-3*pa + 3*pb) / d,
-  c = pa / d;
-
-  var p = (3*b - a*a)/3,
-  p3 = p/3,
-  q = (2*a*a*a - 9*a*b + 27*c)/27,
-  q2 = q/2,
-  discriminant = q2*q2 + p3*p3*p3;
-
-  // and some variables we're going to use later on:
-  var u1,v1,root1,root2,root3;
-
-  // three possible real roots:
-  if (discriminant < 0) {
-    var mp3  = -p/3,
-    mp33 = mp3*mp3*mp3,
-    r    = sqrt( mp33 ),
-    t    = -q / (2*r),
-    cosphi = t<-1 ? -1 : t>1 ? 1 : t,
-    phi  = acos(cosphi),
-    crtr = cuberoot(r),
-    t1   = 2*crtr;
-    root1 = t1 * cos(phi/3) - a/3;
-    root2 = t1 * cos((phi+2*pi)/3) - a/3;
-    root3 = t1 * cos((phi+4*pi)/3) - a/3;
-    return [root1, root2, root3].filter(accept);
-  }
-
-  // three real roots, but two of them are equal:
-  if(discriminant === 0) {
-    u1 = q2 < 0 ? cuberoot(-q2) : -cuberoot(q2);
-    root1 = 2*u1 - a/3;
-    root2 = -u1 - a/3;
-    return [root1, root2].filter(accept);
-  }
-
-  // one real root, two complex roots
-  var sd = sqrt(discriminant);
-  u1 = cuberoot(sd - q2);
-  v1 = cuberoot(sd + q2);
-  root1 = u1 - v1 - a/3;
-  return [root1].filter(accept);
-}
-
-
-

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.

-

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.

-

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 root finding method (yes, after -that - Newton), which we can make use of.

-

The Newton-Raphson approach consists of picking a value t (any will do), and getting the corresponding value 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:

-\[ - t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_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)

-

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?

-

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.

-

In conclusion:

-

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:

- - -
; } - - }, - "boundingbox": { - "locale": "en-GB", - "title": "Bounding boxes", - "getContent": function(handler) { return
- -

If we have the extremities, and the start/end points, a simple for loop that tests for min/max values for x and y means we have the four values we need to box in our curve:

-

-Computing the bounding box for a Bézier curve:

-
    -
  1. Find all t value(s) for the curve derivative's x- and y-roots.
  2. -
  3. Discard any t value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].
  4. -
  5. Determine the lowest and highest value when plugging the values t=0, t=1 and each of the found roots into the original functions: the lowest value is the lower bound, and the highest value is the upper bound for the bounding box we want to construct.
  6. -
-

Applying this approach to our previous root finding, we get the following bounding boxes (with all curve extremity points shown on the curve):

- - -

We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first need to look at how aligning works.

-
; } - - }, - "aligning": { - "locale": "en-GB", - "title": "Aligning curves", - "getContent": function(handler) { return
- -

While there are an incredible number of curves we can define by varying the x- and y-coordinates for the control points, not all curves are actually distinct. For instance, if we define a curve, and then rotate it 90 degrees, it's still the same curve, and we'll find its extremities in the same spots, just at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to "axis-align" it.

-

Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first point lies on (0,0), which turns our n term polynomial functions into n-1 term functions. The order stays the same, but we have less terms. Then, we can rotate the curves so that the last point always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the function for the y-component to an n-2 term function. For instance, if we have a cubic curve such as this:

-\[ -\left \{ \begin{matrix} - x = BLUE[120] \cdot (1-t)^3 BLUE[+ 35] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 220] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 220] \cdot t^3 \\ - y = BLUE[160] \cdot (1-t)^3 BLUE[+ 200] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 260] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 40] \cdot t^3 -\end{matrix} \right. -\]

Then translating it so that the first coordinate lies on (0,0), moving all x coordinates by -120, and all y coordinates by -160, gives us:

-\[ -\left \{ \begin{matrix} - x = BLUE[0] \cdot (1-t)^3 BLUE[- 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 100] \cdot t^3 \\ - y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 120] \cdot t^3 -\end{matrix} \right. -\]

If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:

-\[ -\left \{ \begin{matrix} - x = BLUE[0] \cdot (1-t)^3 BLUE[+ 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 12] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\ - y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 140] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 0] \cdot t^3 -\end{matrix} \right. -\]

If we drop all the zero-terms, this gives us:

-\[ -\left \{ \begin{array}{l} - x = BLUE[85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 13] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\ - y = BLUE[40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 141] \cdot 3 \cdot (1-t) \cdot t^2 -\end{array} \right. -\]

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:

- - -
; } - - }, - "tightbounds": { - "locale": "en-GB", - "title": "Tight boxes", - "getContent": function(handler) { return
- -

With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for curves. We first align our curve, recording the translation we performed, "T", and the rotation angle we used, "R". We then determine the aligned curve's normal bounding box. Once we have that, we can map that bounding box back to our original curve by rotating it by -R, and then translating it by -T. We now have nice tight bounding boxes for our curves:

- - -

These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding precision is often not worth it. If there is high demand for it, I'll add a section on how to precisely compute the best fit bounding box, but the maths is fairly gruelling and just not really worth spending time on.

-
; } - - }, - "inflections": { - "locale": "en-GB", - "title": "Curve inflections", - "getContent": function(handler) { return
- -

Now that we know how to align a curve, there's one more thing we can calculate: inflection points. Imagine we have a variable size circle that we can slide up against our curve. We place it against the curve and adjust its radius so that where it touches the curve, the curvatures of the curve and the circle are the same, and then we start to slide the circle along the curve - for quadratic curves, we can always do this without the circle behaving oddly: we might have to change the radius of the circle as we slide it along, but it'll always sit against the same side of the curve.

-

But what happens with cubic curves? Imagine we have an S curve and we place our circle at the start of the curve, and start sliding it along. For a while we can simply adjust the radius and things will be fine, but once we get to the midpoint of that S, something odd happens: the circle "flips" from one side of the curve to the other side, in order for the curvatures to keep matching. This is called an inflection, and we can find out where those happen relatively easily.

-

What we need to do is solve a simple equation:

-\[ - C(t) = 0 -\]

What we're saying here is that given the curvature function C(t), we want to know for which values of t this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does C(t) look like? Actually something that seems not too hard:

-\[ - C(t) = Bézier_x\prime(t) \cdot Bézier_y{\prime\prime}(t) - Bézier_y\prime(t) \cdot Bézier_x{\prime\prime}(t) -\]

So the function C(t) is wholly defined by the first and second derivative functions for the parametric dimensions of our curve. And as already shown, derivatives of Bézier curves are just simpler Bézier curves, with very easy to compute new coefficients, so this should be pretty easy.

-

However as we've seen in the section on aligning, aligning lets us simplify things a lot, by completely removing the contributions of the first coordinate from most mathematical evaluations, and removing the last y coordinate as well by virtue of the last point lying on the x-axis. So, while we can evaluate C(t) = 0 for our curve, it'll be much easier to first axis-align the curve and then evalutating the curvature function.

- -
-

Let's derive the full formula anyway

-

Of course, before we do our aligned check, let's see what happens if we compute the curvature function without axis-aligning. We start with the first and second derivatives, given our basis functions:

-\[ -\begin{align*} - & Bézier(t) = x_1(1-t)^3 + 3x_2(1-t)^2t + 3x_3(1-t)t^2 + x_4t^3 \\ - & Bézier^\prime(t) = a(1-t)^2 + 2b(1-t)^t + ct^2\ \left\{ a=3(x_2-x_1),b=3(x_3-x_2),c=3(x_4-x_3) \right\} \\ - & Bézier^{\prime\prime}(t) = u(1-t) + vt\ \left\{ u=2(b-a),v=2(c-b) \right\}\ -\end{align*} -\]

And of course the same functions for y:

-\[ -\begin{align*} - & Bézier(t) = y_1(1-t)^3 + 3y_2(1-t)^2t + 3y_3(1-t)t^2 + y_4t^3 \\ - & Bézier^\prime(t) = d(1-t)^2 + 2e(1-t)^t + ft^2\\ - & Bézier^{\prime\prime}(t) = w(1-t) + zt -\end{align*} -\]

Asking a computer to now compose the C(t) function for us (and to expand it to a readable form of simple terms) gives us this rather overly complicated set of arithmetic expressions:

-\[ -\begin{array} - -18 t^2 x_2 y_1+36 t^2 x_3 y_1-18 t^2 x_4 y_1+18 t^2 x_1 y_2-54 t^2 x_3 y_2 \\ - +36 t^2 x_4 y_2-36 t^2 x_1 y_3+54 t^2 x_2 y_3-18 t^2 x_4 y_3+18 t^2 x_1 y_4 \\ - -36 t^2 x_2 y_4+18 t^2 x_3 y_4+36 t x_2 y_1-54 t x_3 y_1+18 t x_4 y_1-36 t x_1 y_2 \\ - +54 t x_3 y_2-18 t x_4 y_2+54 t x_1 y_3-54 t x_2 y_3-18 t x_1 y_4+18 t x_2 y_4 \\ - -18 x_2 y_1+18 x_3 y_1+18 x_1 y_2-18 x_3 y_2-18 x_1 y_3+18 x_2 y_3 -\end{array} -\]

That is... unwieldy. So, we note that there are a lot of terms that involve multiplications involving x1, y1, and y4, which would all disappear if we axis-align our curve, which is why aligning is a great idea.

-
-

Aligning our curve so that three of the eight coefficients become zero, we end up with the following simple term function for C(t):

-\[ - 18 \left ( (3 x_3 y_2+2 x_4 y_2+3 x_2 y_3-x_4 y_3)t^2 + (3 x_3 y_2-x_4 y_2-3 x_2 y_3)t + (x_2 y_3-x_3 y_2) \right ) -\]

That's a lot easier to work with: we see a fair number of terms that we can compute and then cache, giving us the following simplification:

-\[ - \left.\begin{matrix} - a = x_3 \cdot y_2 \\ - b = x_4 \cdot y_2 \\ - c = x_2 \cdot y_3 \\ - d = x_4 \cdot y_3 - \end{matrix}\right\} - \ C(t) = 18 \cdot \left ( (-3a + 2b + 3c - d)t^2 + (3a - b - 3c)t + (c - a) \right ) -\]

This is a plain quadratic curve, and we know how to solve C(t) = 0; we use the quadratic formula:

-\[ - \left.\begin{matrix} - x =& 18(-3a + 2b + 3c - d) \\ - y =& 18(3a - b - 3c) \\ - z =& 18(c - a) - \end{matrix}\right\} - \ C(t) = 0 \ \Rightarrow\ t = \frac{-y \pm \sqrt{y^2 - 4 x z}}{2x} -\]

We can easily compute this value if the descriminator isn't a negative number (because we only want real roots, not complex roots), and if x is not zero, because divisions by zero are rather useless.

-

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.

- -
; } - - }, - "canonical": { - "locale": "en-GB", - "title": "Canonical form (for cubic curves)", - "getContent": function(handler) { return
- -

While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve. As a curvature controlled by more than one control points, it exhibits all kinds of features like loops, cusps, odd colinear features, and up to two inflection points because the curvature can change direction up to three times. Now, knowing what kind of curve we're dealing with means that some algorithms can be run more efficiently than if we have to implement them as generic solvers, so is there a way to determine the curve type without lots of work?

-

As it so happens, the answer is yes and the solution we're going to look at was presented by Maureen C. Stone from Xerox PARC and Tony D. deRose from the University of Washington in their joint paper "A Geometric Characterization of Parametric Cubic curves". It was published in 1989, and defines curves as having a "canonical" form (i.e. a form that all curves can be reduced to) from which we can immediately tell which features a curve will have. So how does it work?

-

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:

- -

This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0), (0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have certain features. Namely, if the fourth point is...

-
    -
  1. anywhere on and in the red zone, the curve will be self-intersecting, yielding either a cusp or a loop. Anywhere inside the the red zone, this will be a loop. We won't know where that loop is (in terms of t values), but we are guaranteed that there is one.
  2. -
  3. on the left (red) edge, the curve will have a cusp. We again don't know where, just that it -has one. This edge is described by the function:
  4. -
-\[ - y = \frac{-x^2 + 2x + 3}{4}, \{ x \leq 1 \} - \]
    -
  1. on the lower right (pink) edge, the curve will have a loop at t=1, so we know the end coordinate of -the curve also lies on the curve. This edge is described by the function:
  2. -
-\[ - y = \frac{\sqrt{3(4x - x^2)} - x}{2}, \{ 0 \leq x \leq 1 \} - \]
    -
  1. on the top (blue) edge, the curve will have a loop at t=0, so we know the start coordinate of -the curve also lies on the curve. This edge is described by the function:
  2. -
-\[ - y = \frac{-x^2 + 3x}{3}, \{ x \leq 0 \} - \]
    -
  1. inside the green zone, the curve will have a single inflection, switching concave/convex once.
  2. -
  3. between the red and green zones, the curve has two inflections, meaning its curvature switches between -concave/convex form twice.
  4. -
  5. anywhere on the right of the red zone, the curve will have no inflections. It'll just be a well-behaved arch.
  6. -
-

Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.

- -
-

Wait, where do those lines come from?

-

Without repeating the paper mentioned at the top of this section, the loop-boundaries come from rewriting the curve into canonical form, and then solving the formulae for which constraints must hold for which possible curve properties. In the paper these functions yield formulae for where you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived for the full cubic expression, meaning they apply to t=-∞ to t=∞... For Bézier curves we only care about the "clipped interval" t=0 to t=1, so some of the properties that apply when you look at the curve over an infinite interval simply don't apply to the Bézier curve interval.

-

The right bound for the loop region, indicating where the curve switches from "having inflections" to "having a loop", for the general cubic curve, is actually mirrored over x=1, but for Bézier curves this right half doesn't apply, so we don't need to pay attention to it. Similarly, the boundaries for t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general curve does over the interval t=0 to t=1.

-

For the full details, head over to the paper and read through sections 3 and 4. If you still remember your high school precalculus, you can probably follow along with this paper, although you might have to read it a few times before all the bits "click".

-
-

So now the question becomes: how do we manipulate our curve so that it fits this canonical form, with three fixed points, and one "free" point? Enter linear algerba. Don't worry, I'll be doing all the math for you, as well as show you what the effect is on our curves, but basically we're going to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus approach is very hard to work with, when the equivalent geometrical solution is super obvious.

-

The approach is going to start with a curve that doesn't have all-colinear points (so we need to make sure the points don't all fall on a straight line), and then applying four graphics operations that you will probably have heard of: translation (moving all points by some fixed x- and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an operation that turns rectangles into parallelograms).

-

Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going to make use of an interesting trick here, by pretending our 2D coordinates are 3D, with the z coordinate simply always being 1. This is an old trick in graphics to overcome the limitations of 2D transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form (ax + by, cx + dy), which means we can't do translation, since that requires we end up with some kind of (x + a, y + b). If we add a bogus z coordinate that is always 1, then we can suddenly add arbitrary values. For example:

-\[ -\left [ \begin{array} - 01 & 0 & a \\ - 0 & 1 & b \\ - 0 & 0 & 1 - \end{array} \right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - z=1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - 1 \cdot x + 0 \cdot y + a \cdot z \\ - 0 \cdot x + 1 \cdot y + b \cdot z \\ - 0 \cdot x + 0 \cdot y + 1 \cdot z - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + a \cdot 1 \\ - y + b \cdot 1 \\ - 1 \cdot z - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + a \\ - y + b \\ - z=1 - \end{matrix} -\right ] -\]

Sweet! z stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:

-\[ -T_1 = -\left [ \begin{array} - 01 & 0 & -{P_1}_x \\ - 0 & 1 & -{P_1}_y \\ - 0 & 0 & 1 - \end{array} \right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - 1 \cdot x + 0 \cdot y - {P_1}_x \cdot 1 \\ - 0 \cdot x + 1 \cdot y - {P_1}_y \cdot 1 \\ - 0 \cdot x + 0 \cdot y + 1 \cdot 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x - {P_1}_x \\ - y - {P_1}_y \\ - 1 - \end{matrix} -\right ] -\]

Running all our coordinates through this transformation gives a new set of coordinates, let's call those U, where the first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the x=0 line, so what we want is a transformation matrix that, when we run it, subtracts x from whatever x we currently have. This is called shearing, and the typical x-shear matrix and its transformation looks like this:

-\[ -\left [ - \begin{matrix} - 1 & S & 0 \\ - 0 & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + S \cdot y \\ - y \\ - 1 - \end{matrix} -\right ] -\]

So we want some shearing value that, when multiplied by y, yields -x, so our x coordinate becomes zero. That value is simpy -x/y, because -x/y * y = -x. Done:

-\[ -T_2 = -\left [ - \begin{matrix} - 1 & -\frac{ {U_2}_x }{ {U_2}_y } & 0 \\ - 0 & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on (0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to do some scaling to make sure it ends up at (0,1). Additionally, we want point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3x, and y-scaling by point2y. This is really easy:

-\[ -T_3 = -\left [ - \begin{matrix} - \frac{1}{ {V_3}_x } & 0 & 0 \\ - 0 & \frac{1}{ {V_2}_y } & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared point 2 earlier. In this case, we do the same trick, but with y/x rather than x/y because we're not x-shearing but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:

-\[ -T_4 = -\left [ - \begin{matrix} - 1 & 0 & 0 \\ - \frac{1 - {W_3}_y}{ {W_3}_x } & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:

-\[ -mapped_4 = \left ( - \begin{matrix} - x = \left ( - \frac - { - -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} - } - { - -x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2} - } - \right ) -\\ - y = \left ( - \frac{(-y_1+y_4)}{-y_1+y_2} - + - \frac - { - \left ( 1 - \frac{-y_1+y_3}{-y_1+y_2} \right ) - \left ( -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} \right ) - } - { - -x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2} - } - \right ) - \end{matrix} -\right ) -\]

That looks very complex, but notice that every coordinate value is being offset by the initial translation, and a lot of terms in there repeat: it's pretty easy to calculate this fast, since there's so much we can cache and reuse while we compute this mapped coordinate!

-

First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?

-\[ -... = \left ( - \begin{matrix} - x = \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right ) -\\ - y = - \frac{y_4}{y_2} - + - \left ( 1 - \frac{y_3}{y_2} \right ) - \cdot - \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right ) - \end{matrix} -\right ) -\]

Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:

-\[ -... = \left ( - \begin{matrix} - x = (x_4 - x_2 \cdot f_{42}) / ( x_3- x_2 \cdot f_{32} ) -\\ - y = - f_{42} - + - \left ( 1 - f_{32} \right ) - \cdot - x - \end{matrix} -\right ), f_{32} = \frac{y_3}{y_2}, f_{42} = \frac{y_4}{y_2} -\]

That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it look!

- -
-

How do you track all that?

-

Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply use Mathematica. Tracking all this math by hand is insane, and we invented computers, literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the program do the math for me. And real math, too, with symbols, not with numbers. In fact, here's the Mathematica notebook if you want to see how this works for yourself.

-

Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's $295 for home use, but it's also free when you buy a $35 raspberry pi. Obviously, I bought a raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to do, Mathematica can just do it for you. And we don't have to be geniusses to work out what the maths looks like. That's what we have computers for.

-
-

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:

- -
; } - - }, - "arclength": { - "locale": "en-GB", - "title": "Unknown title (arclength)", - "getContent": function(handler) { return
-
; } - - }, - "arclengthapprox": { - "locale": "en-GB", - "title": "Unknown title (arclengthapprox)", - "getContent": function(handler) { return
-
; } - - }, - "tracing": { - "locale": "en-GB", - "title": "Unknown title (tracing)", - "getContent": function(handler) { return
-
; } - - }, - "intersections": { - "locale": "en-GB", - "title": "Unknown title (intersections)", - "getContent": function(handler) { return
-
; } - - }, - "curveintersection": { - "locale": "en-GB", - "title": "Unknown title (curveintersection)", - "getContent": function(handler) { return
-
; } - - }, - "abc": { - "locale": "en-GB", - "title": "Unknown title (abc)", - "getContent": function(handler) { return
-
; } - - }, - "moulding": { - "locale": "en-GB", - "title": "Unknown title (moulding)", - "getContent": function(handler) { return
-
; } - - }, - "pointcurves": { - "locale": "en-GB", - "title": "Unknown title (pointcurves)", - "getContent": function(handler) { return
-
; } - - }, - "catmullconv": { - "locale": "en-GB", - "title": "Unknown title (catmullconv)", - "getContent": function(handler) { return
-
; } - - }, - "catmullmoulding": { - "locale": "en-GB", - "title": "Unknown title (catmullmoulding)", - "getContent": function(handler) { return
-
; } - - }, - "polybezier": { - "locale": "en-GB", - "title": "Unknown title (polybezier)", - "getContent": function(handler) { return
-
; } - - }, - "shapes": { - "locale": "en-GB", - "title": "Unknown title (shapes)", - "getContent": function(handler) { return
-
; } - - }, - "projections": { - "locale": "en-GB", - "title": "Unknown title (projections)", - "getContent": function(handler) { return
-
; } - - }, - "offsetting": { - "locale": "en-GB", - "title": "Unknown title (offsetting)", - "getContent": function(handler) { return
-
; } - - }, - "graduatedoffset": { - "locale": "en-GB", - "title": "Unknown title (graduatedoffset)", - "getContent": function(handler) { return
-
; } - - }, - "circles": { - "locale": "en-GB", - "title": "Unknown title (circles)", - "getContent": function(handler) { return
-
; } - - }, - "circles_cubic": { - "locale": "en-GB", - "title": "Unknown title (circles_cubic)", - "getContent": function(handler) { return
-
; } - - }, - "arcapproximation": { - "locale": "en-GB", - "title": "Unknown title (arcapproximation)", - "getContent": function(handler) { return
-
; } - - }, - "bsplines": { - "locale": "en-GB", - "title": "Unknown title (bsplines)", - "getContent": function(handler) { return
-
; } - - }, - "comments": { - "locale": "en-GB", - "title": "Unknown title (comments)", - "getContent": function(handler) { return
-
; } - - }, - "locale-switcher": { - "locale": "en-GB", - "title": "locale-switcher", - "getContent": function(handler) { return
-

Read this in your own language:

- -

Don't see your language listed? Help translate this content! -

-
; } - - } -}; diff --git a/locales/ja-JP/content.js b/locales/ja-JP/content.js deleted file mode 100644 index ea8cbf3a..00000000 --- a/locales/ja-JP/content.js +++ /dev/null @@ -1,1902 +0,0 @@ -var React = require('react'); -var Graphic = require("../../components/Graphic.jsx"); -var SectionHeader = require("../../components/SectionHeader.jsx"); - -module.exports = { - "preface": { - "locale": "ja-JP", - "title": "まえがき", - "getContent": function(handler) { return
- -

2次元上になにかを描くとき、普通は線を使いますが、これは直線と曲線の2つに分類することができます。直線を描くのはとても簡単で、これをコンピュータに描かせるのも容易です。直線の始点と終点をコンピュータに与えてやれば、ポン!直線が描けました。疑問の余地もありません。

-

しかしながら、曲線の方はもっと大きな問題です。私たちはフリーハンドでいとも簡単に曲線を描くことができますが、コンピュータの方は少し不利です。曲線の描き方を表した数学的な関数が与えられないと、コンピュータは曲線を描くことができないのです。実際には、直線でさえも関数が必要になります。直線の関数はとても簡単なので、わたしたちはよく無視してしまいますが、コンピュータにとっては直線であれ曲線であれ、線はすべて「関数」なのです。しかしこれは、コンピュータで速く計算できて、きれいな曲線が得られるような関数を見つける必要がある、ということになります。そのような関数はたくさんありますが、多くの関心を集め続け、そしてどんな場面でも使われている、ある特定の関数に対してこの記事では焦点を絞ります。この関数は「ベジエ」曲線を描きます。

-

ベジエ曲線はPierre Bézierから名付けられました。この曲線がデザイン作業に適していることを世界に知らしめたのが、彼なのです(ルノーに勤務し、1962年にその研究を発表しました)。ただし、この曲線を「発明」したのは彼が最初で唯一というわけではありません。数学者Paul de Casteljauはシトロエンで働いていた1959年、この曲線の性質について研究し、ベジエ曲線の非常にエレガントな描き方を発見しました。これが最初だと言う人もいます。しかしながら、de Casteljauは自分の成果を発表しなかったため、「誰が最初か?」という問いに答えるのがとても難しくなっています。またベジエ曲線は、核心的にはSergei Natanovich Bernsteinが研究した「ベルンシュタイン多項式」という数学関数の一種ですが、こちらの公刊に関しては少なくとも1912年まで遡ることができます。いずれにせよ、これらはほとんど瑣末なことです。より注目すべきなのは、ベジエ曲線は取り扱いに便利だいうことです。たとえば複数のベジエ曲線を繋いで、1つの曲線に見えるようにすることができます。もしあなたがPhotoshopで「パス」を描いたり、FlashやIllustrator、Inkscapeのようなベクタードローイングソフトを使ったことがあるのであれば、そこで描いてきた曲線はベジエ曲線です。

-

では、これを自分でプログラムしなければならないとなったらどうでしょう?ハマりどころは何でしょうか?どうやってベジエ曲線を描くのでしょう?バウンディングボックスとは何で、どうやって交点を求め、どうやったら曲線を押し出せるのでしょうか?つまるところ、ベジエ曲線に対して行いたいあらゆる操作は、どのようにすればいいのでしょう?このページはそれに答えるためにあります。数学にとりかかりましょう!

-

—Pomax (Twitter上では@TheRealPomax)

- -
-

注:ベジエの図はすべてインタラクティブになっています。

-

このページではBezier.jsを活用し、インタラクティブな例示を行っています。また、MathJaxというすばらしいライブラリによって、(LaTeX式の)「本物」の数学組版を行っています。このページはWebpackを使い、Reactアプリケーションとしてオフラインで生成されていますが、このために「ソースを表示」オプションを追加することがかなり難しくなってしまいました。これをどうやって追加すべきかは今もまだ考え中ですが、かといって、数年ぶりとなる今回の更新を延期したくはありませんでした。

-

本書はオープンソースです。

-

この本はオープンソースソフトウェアのプロジェクトで、2つのGitHubリポジトリ上に存在しています。1つ目のhttps://github.com/pomax/bezierinfoは表示のためだけに用意されたバージョンで、あなたが今読んでいるものです。もう一方のhttps://github.com/pomax/BezierInfo-2は開発版で、すべてのHTML・JavaScript・CSSが含まれています。どちらのリポジトリもフォークすることができますし、あなたの好きなように使ってかまいません。ただし、これを自分の成果だと騙って売ることはもちろん除きます。=)

-

数学はどこまで難しくなりますか?

-

この入門に出てくる数学は、大半が高校初年度程度です。基本的な計算を理解していて、英語の読み方が分かっていれば、こなすことができるはずです。時にはずっと難しい数学も出てきますが、もし理解できないように感じたら、そこは読み飛ばしても大丈夫です。節の中の「詳細欄」を飛ばしてもいいですし、厄介そうな数学があれば節の最後まで読み飛ばしてもかまいません。各節の最後にはたいてい結論を並べてありますので、これを直に利用することもできます。

-

質問・コメント:

-

新しい節の提案があれば、GitHubのissueトラッカーをクリックしてください(右上にあるリポジトリのリンクからでもたどり着けます)。改訂中のため、現在はこのページにはコメント欄がありませんが、内容に関する質問がある場合にもissueトラッカーを使ってかまいません。改訂が完了したら、総合的なコメント欄を復活させる予定です。あるいは、「質問対象の節を選択して『質問』ボタンをクリック」するような項目別のシステムになるかもしれません。いずれわかります。

-

コーヒーをおごってくれませんか?

-

この本が気に入った場合や、取り組んでいたことに役に立つと思った場合、あるいは、この本への感謝をわたしに伝えるにはどうすればいいのかわからない場合。そのような場合には、わたしにコーヒーをおごってください。あなたが住んでいるところのコーヒー1杯の値段でかまいません。この本は小さな入門からはじまり、印刷で70ページほどに相当するようなベジエ曲線の読み物へと、年々成長してきています。そして、多くのコーヒーが執筆に費やされてきました。わたしは執筆に使った時間が惜しいとは思いませんが、もう少しコーヒーがあれば、ずっと書き続けることができるのです!

-
-
; } - - }, - "introduction": { - "locale": "ja-JP", - "title": "バッとした導入", - "getContent": function(handler) { return
- -

まずは良い例から始めましょう。ベジエ曲線というのは、下の図に表示されているもののことです。ベジエ曲線はある始点からある終点へと延びており、その曲率は1個以上の「中間」制御点に左右されています。さて、このページの図はどれもインタラクティブになっていますので、ここで曲線をちょっと操作してみましょう。点をドラッグしたとき、曲線の形がそれに応じてどう変化するのか、確かめてみてください。

- -
- - -
-

ベジエ曲線は、CAD(computer aided designやCAM(computer aided manufacturing)のアプリケーションで多用されています。もちろん、Adobe Illustrator・Photoshop・Inkscape・Gimp などのグラフィックデザインアプリケーションや、SVG(scalable vector graphics)・OpenTypeフォント(otf/ttf)のようなグラフィック技術でも利用されています。ベジエ曲線はたくさんのものに使われていますので、これについてもっと詳しく学びたいのであれば……さあ、準備しましょう!

-
; } - - }, - "whatis": { - "locale": "ja-JP", - "title": "ではベジエ曲線はどうやってできるのでしょう?", - "getContent": function(handler) { return
- -

ベジエ曲線がどのように動くのか、点を触ってみて感覚が摑めたかもしれません。では、実際のところベジエ曲線とはいったい何でしょうか?これを説明する方法は2通りありますが、どちらの説明でも行き着く先はまったく同じです。一方は複雑な数学を使うのに対し、もう一方はとても簡単です。というわけで……簡単な説明の方から始めましょう。

-

ベジエ曲線は、線形補間の結果です。というと難しそうに聞こえますが、誰でも幼い頃から線形補間をやってきています。例えば、何か2つのものの間を指し示すときには、いつも線形補間を行っています。線形補間とは、単純に「2点の間から点を得る」ことなのです。

-

例えば、2点間の距離がわかっているとして、一方の点から距離の20%だけ離れた(すなわち、もう一方の点から80%離れた)新しい点を求めたい場合、次のようにとても簡単に計算できます。

-\[ -Given \left ( - \begin{align} - p_1 &= some\ point \\ - p_2 &= some\ other\ point \\ - distance &= (p_2 - p_1) \\ - ratio &= \frac{percentage}{100} \\ - \end{align} -\right ),\ our\ new\ point = p_1 + distance \cdot ratio -\]

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

- -

また、これが複雑な方の数学につながっていきます。微積分です。

-

いま上で行ったものとは似つかないように思えますが、実はあれは2次曲線を描いていたのです。ただし一発で描きあげるのではなく、手順を追って描いていきました。ベジエ曲線は多項式関数で表現できますが、その一方で、とても単純な補間の補間の補間の……というふうにも説明できます。これがベジエ曲線のおもしろいところです。これはまた、ベジェ曲線は「本当の数学」で見る(関数を調べたり微分を調べたり、あらゆる方法で)ことも可能ですし、「機械的」な組み立て操作として見る(例えば、ベジエ曲線は組み立てに使う点の間からは決してはみ出ないということがわかります)ことも可能だということを意味しています。

-

それでは、もう少し詳しくベジエ曲線を見ていきましょう。数学的な表現やそこから導かれる性質、さらには、ベジエ曲線に対して/ベジエ曲線を使ってできるさまざまな内容についてです。

-
; } - - }, - "explanation": { - "locale": "ja-JP", - "title": "ベジエ曲線の数学", - "getContent": function(handler) { return
- -

ベジエ曲線は「パラメトリック」関数の一種です。数学的に言えば、パラメトリック関数というのはインチキです。というのも、「関数」はきっちり定義された用語であり、いくつかの入力を1つの出力に対応させる写像を表すものだからです。いくつかの数値を入れると、1つの数値が出てきます。入れる数値が変わっても、出てくる数値はやはり1つだけです。パラメトリック関数はインチキです。基本的には「じゃあわかった、値を複数個出したいから、関数を複数個使うことにするよ」ということです。例として、ある値xに何らかの操作を行い、別の値へと写す関数があるとします。

-\[ - f(x) = \cos(x) -\]

-f(x)という記法は、これが関数(1つしかない場合は慣習的にfと呼びます)であり、その出力が1つの変数(この場合はxです)に応じて変化する、ということを示す標準的な方法です。xを変化させると、f(x)の出力が変化します。

-

ここまでは順調です。では、パラメトリック関数について、これがどうインチキなのかを見てみましょう。以下の2つの関数を考えます。

-\[ -\begin{matrix} - f(a) = \cos(a) \\ - f(b) = \sin(b) -\end{matrix} -\]

注目すべき箇所は特に何もありません。ただの正弦関数と余弦関数です。ただし、入力が別々の名前になっていることに気づくでしょう。仮にaの値を変えたとしても、f(b)の出力の値は変わらないはずです。なぜなら、こちらの関数にはaは使われていないからです。パラメトリック関数は、これを変えてしまうのでインチキなのです。パラメトリック関数においては、どの関数も変数を共有しています。例えば、

-\[ -\left \{ \begin{matrix} - f_a(t) = \cos(t) \\ - f_b(t) = \sin(t) -\end{matrix} \right. -\]

複数の関数がありますが、変数は1つだけです。tの値を変えた場合、fa(t)fb(t)の両方の出力が変わります。これがどのように役に立つのか、疑問に思うかもしれません。しかし、実際には答えは至ってシンプルです。fa(t)fb(t)のラベルを、パラメトリック曲線の表示によく使われているもので置き換えてやれば、ぐっとはっきりするかと思います。

-\[ -\left \{ \begin{matrix} - x = \cos(t) \\ - y = \sin(t) -\end{matrix} \right. -\]

きました。x/y座標です。謎の値tを通して繫がっています。

-

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

- -

ベジエ曲線はパラメトリック関数の一種であり、どの次元に対しても同じ基底関数を使うという点で特徴づけられます。先ほどの例では、xの値とyの値とで異なる関数(正弦関数と余弦関数)を使っていましたが、ベジエ曲線ではxyの両方で「二項係数多項式」を使います。では、二項係数多項式とは何でしょう?

-

高校で習った、こんな形の多項式を思い出すかもしれません。

-\[ - f(x) = a \cdot x^3 + b \cdot x^2 + c \cdot x + d -\]

最高次の項がであれば3次多項式、であれば2次多項式と呼び、xだけの場合は1次多項式――ただの直線です。(そしてxの入った項が何もなければ、多項式ではありません!)

-

ベジエ曲線はxの多項式ではなく、tの多項式です。tの値は0から1までの間に制限され、その係数abなどは「二項係数」の形をとります。というと複雑そうに聞こえますが、実際には値を組み合わせて、とてもシンプルに記述できます。

-\[ -\begin{align*} - linear &= (1-t) + t \\ - square &= (1-t)^2 + 2 \cdot (1-t) \cdot t + t^2 \\ - cubic &= (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3 -\end{align*} -\]

「そこまでシンプルには見えないよ」と思っていることでしょう。しかし仮に、tを取り去って係数に1を掛けることにしてしまえば、急激に簡単になります。これが二項係数部分の項です。

-\[ -\begin{align*} - linear &= \hskip{2.5em} 1 + 1 \\ - square &= \hskip{1.7em} 1 + 2 + 1\\ - cubic &= \hskip{0.85em} 1 + 3 + 3 + 1\\ - hypercubic &= 1 + 4 + 6 + 4 + 1 -\end{align*} -\]

2は1+1に等しく、3は2+1や1+2に等しく、6は3+3に等しく、……ということに注目してください。見てわかるように、先頭と末尾は単に1になっていますが、中間はどれも次数が増えるたびに「上の2つの数を足し合わせた」ものになっています。これなら覚えやいですね。

-

多項式部分の項がどうなっているのか、同じぐらい簡単な方法で考えることができます。仮に、(1-t)aに、tbに書き換え、さらに重みを一旦削除してしまえば、このようになります。

-\[ -\begin{align*} - linear &= BLUE[a] + RED[b] \\ - square &= BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot RED[b] + RED[b] \cdot RED[b] \\ - cubic &= BLUE[a] \cdot BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot BLUE[a] \cdot RED[b] + BLUE[a] \cdot RED[b] \cdot RED[b] + RED[b] \cdot RED[b] \cdot RED[b]\\ -\end{align*} -\]

これは要するに、「abのすべての組み合わせ」の単なる和です。プラスが出てくるたびに、abへと1つずつ置き換えていけばよいのです。こちらも本当に単純です。さて、これで「二項係数多項式」がわかりました。完璧を期するため、この関数の一般の形を示しておきます。

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} -\]

そして、これがベジエ曲線の完全な表現です。この関数中のΣは、加算の繰り返し(Σの下にある変数を使って、...=<値>から始めてΣの下にある値まで)を表します。

- -
-

基底関数の実装方法

-

上で説明した関数を使えば、数学的な組み立て方で、基底関数をナイーブに実装することもできます。

-
function Bezier(n,t):
-  sum = 0
-  for(k=0; k<n; k++):
-    sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

「こともできる」と書いたのは、この方法では実装しない方が良いからです。階乗はとてつもなく重い計算なのです。また、先ほどの説明からわかるように、実際は階乗を使わなくても、かなり簡単にパスカルの三角形を作ることができます。[1]から始めて[1,1]、[1,2,1]、[1,3,3,1]、……としていくだけです。下の段は上の段よりも1つ要素が増え、各段の先頭と末尾は1になります。中間の数はどれも、左右斜め上にある両要素の和になります。

-

このパスカルの三角形は、「リストのリスト」として瞬時に生成できます。そして、これをルックアップテーブルとして利用すれば、二項係数を計算する必要はまったくなくなります。

-
lut = [      [1],           // n=0
-            [1,1],          // n=1
-           [1,2,1],         // n=2
-          [1,3,3,1],        // n=3
-         [1,4,6,4,1],       // n=4
-        [1,5,10,10,5,1],    // n=5
-       [1,6,15,20,15,6,1]]  // n=6
-
-binomial(n,k):
-  while(n >= lut.length):
-    s = lut.length
-    nextRow = new array(size=s+1)
-    nextRow[0] = 1
-    for(i=1, prev=s-1; i<prev; i++):
-      nextRow[i] = lut[prev][i-1] + lut[prev][i]
-    nextRow[s] = 1
-    lut.add(nextRow)
-  return lut[n][k]
-
-

これはどのように動くのでしょう?最初に、十分に大きなサイズのルックアップテーブルを宣言します。次に、求めたい値を得るための関数を定義します。この関数は、求めたい値のn/kのペアがテーブル中にまだ存在しない場合、先にテーブルを拡張するようになっています。さて、これで基底関数は次のようになりました。

-
function Bezier(n,t):
-  sum = 0
-  for(k=0; k<=n; k++):
-    sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

完璧です。もちろん、さらなる最適化を施すこともできます。コンピュータグラフィクス用途ではたいてい、任意の次数の曲線が必要になるわけではありません。2次と3次の曲線だけが必要であれば、以下のようにコードを劇的に単純化することができます(実際、この入門では任意の次数までは扱いませんので、これに似たようなコードが出てきます)。

-
function Bezier(2,t):
-  t2 = t * t
-  mt = 1-t
-  mt2 = mt * mt
-  return mt2 + 2*mt*t + t2
-
-function Bezier(3,t):
-  t2 = t * t
-  t3 = t2 * t
-  mt = 1-t
-  mt2 = mt * mt
-  mt3 = mt2 * mt
-  return mt3 + 3*mt2*t + 3*mt*t2 + t3
-
-

これで基底関数をプログラムする方法がわかりました。すばらしい。

-
-

というわけで、基底関数がどのようなものか理解できました。今度はベジエ曲線を特別にする魔法――制御点を導入する時間です。

-
; } - - }, - "control": { - "locale": "en-GB", - "title": "Controlling Bézier curvatures", - "getContent": function(handler) { return
- -

Bézier curves are (like all "splines") interpolation functions, meaning they take a set of points, and generate values somewhere "between" those points. (One of the consequences of this is that you'll never be able to generate a point that lies outside the outline for the control points, commonly called the "hull" for the curve. Useful information!). In fact, we can visualize how each point contributes to the value generated by the function, so we can see which points are important, where, in the curve.

-

The following graphs show the interpolation functions for quadratic and cubic curves, with "S" being the strength of a point's contribution to the total sum of the Bézier function. Click or click-drag to see the interpolation percentages for each curve-defining point at a specific t value.

- -
- - - -
-

Also shown is the interpolation function for a 15th order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.

-

If we want to change the curve, we need to change the weights of each point, effectively changing the interpolations. The way to do this is about as straight forward as possible: just multiply each point with a value that changes its strength. These values are conventionally called "Weights", and we can add them to our original Bézier function:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} - \cdot\ - \underset{weight}{\underbrace{w_i}} -\]

That looks complicated, but as it so happens, the "weights" are actually just the coordinate values we want our curve to have: for an nth - order curve, w0 is our start coordinate, wn is our last coordinate, and everything in between is a controlling coordinate. Say we want a cubic curve that starts at (120,160), is controlled by (35,200) and (220,260) and ends at (220,40), we use this Bézier curve:

-\[ -\left \{ \begin{matrix} - x = BLUE[120] \cdot (1-t)^3 + BLUE[35] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[220] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[220] \cdot t^3 \\ - y = BLUE[160] \cdot (1-t)^3 + BLUE[200] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[260] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[40] \cdot t^3 -\end{matrix} \right. -\]

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

- -

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.

- -
-

How to implement the weighted basis function

-

Given that we already know how to implement basis function, adding in the control points is remarkably easy:

-
function Bezier(n,t,w[]):
-  sum = 0
-  for(k=0; k<n; k++):
-    sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

And for the extremely optimized versions:

-
function Bezier(2,t,w[]):
-  t2 = t * t
-  mt = 1-t
-  mt2 = mt * mt
-  return w[0]*mt2 + w[1]*2*mt*t + w[2]*t2
-
-function Bezier(3,t,w[]):
-  t2 = t * t
-  t3 = t2 * t
-  mt = 1-t
-  mt2 = mt * mt
-  mt3 = mt2 * mt
-  return w[0]*mt3 + 3*w[1]*mt2*t + 3*w[2]*mt*t2 + w[3]*t3
-
-

And now we know how to program the weighted basis function.

-
-
; } - - }, - "extended": { - "locale": "en-GB", - "title": "The Bézier interval [0,1]", - "getContent": function(handler) { return
- -

Now that we know the mathematics behind Bézier curves, there's one curious thing that you may have noticed: they always run from t=0 to t=1. Why that particular interval?

-

It all has to do with how we run from "the start" of our curve to "the end" of our curve. If we have a value that is a mixture of two other values, then the general formula for this is:

-\[ - mixture = a \cdot value_1 + b \cdot value_2 -\]

The obvious start and end values here need to be a=1, b=0, so that the mixed value is 100% value 1, and 0% value 2, and a=0, b=1, so that the mixed value is 0% value 1 and 100% value 2. Additionally, we don't want "a" and "b" to be independent: if they are, then we could just pick whatever values we like, and end up with a mixed value that is, for example, 100% value 1 and 100% value 2. In principle that's fine, but for Bézier curves we always want mixed values between the start and end point, so we need to make sure we can never set "a" and "b" to some values that lead to a mix value that sums to more than 100%. And that's easy:

-\[ - m = a \cdot value_1 + (1 - a) \cdot value_2 -\]

With this we can guarantee that we never sum above 100%. By restricting a to values in the interval [0,1], we will always be somewhere between our two values (inclusively), and we will always sum to a 100% mix.

-

But... what if we use this form, used in the assumption that we will only ever use values between 0 and 1, and instead use values outside of that interval? Do things go horribly wrong? Well... not really, but we get to "see more".

-

In the case of Bézier curves, extending the interval simply makes our curve "keep going". Bézier curves are simply segments on some polynomial curve, so if we pick a wider interval we simply get to see more of the curve. So what do they look like?

-

The following two graphics show you Bézier curves rendered "the usual way", as well as the curves they "lie on" if we were to extend the t values much further. As you can see, there's a lot more "shape" hidden in the rest of the curve, and we can model those parts by moving the curve points around.

- - -

In fact, there are curves used in graphics design and computer modelling that do the opposite of Bézier curves, where rather than fixing the interval, and giving you free coordinates, they fix the coordinates, but give you freedom over the interval. A great example of this is the "Spiro" curve, which is a curve based on part of a Cornu Spiral, also known as Euler's Spiral. It's a very aesthetically pleasing curve and you'll find it in quite a few graphics packages like FontForge and Inkscape, having even been used in font design (such as for the Inconsolata font).

-
; } - - }, - "matrix": { - "locale": "en-GB", - "title": "Bézier curvatures as matrix operations", - "getContent": function(handler) { return
- -

We can also represent Bézier as matrix operations, by expressing the Bézier formula as a polynomial basis function, the weight matrix, and the actual coordinates as matrix. Let's look at what this means for the cubic curve:

-\[ - B(t) = P_1 \cdot (1-t)^3 + P_2 \cdot 3 \cdot (1-t)^2 \cdot t + P_3 \cdot 3 \cdot (1-t) \cdot t^2 + P_4 \cdot t^3 -\]

Disregarding our actual coordinates for a moment, we have:

-\[ - B(t) = (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3 -\]

We can write this as a sum of four expressions:

-\[ - \begin{matrix} - ... & = & (1-t)^3 \\ - & + & 3 \cdot (1-t)^2 \cdot t \\ - & + & 3 \cdot (1-t) \cdot t^2 \\ - & + & t^3 \\ - \end{matrix} -\]

And we can expand these expressions:

-\[ - \begin{matrix} - ... & = & (1-t) \cdot (1-t) \cdot (1-t) & = & -t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\ - & + & 3 \cdot (1-t) \cdot (1-t) \cdot t & = & 3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t \\ - & + & 3 \cdot (1-t) \cdot t \cdot t & = & -3 \cdot t^3 + 3 \cdot t^2 \\ - & + & t \cdot t \cdot t & = & t^3 \\ - \end{matrix} -\]

Furthermore, we can make all the 1 and 0 factors explicit:

-\[ - \begin{matrix} - ... & = & -1 \cdot t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\ - & + & +3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t + 0 \\ - & + & -3 \cdot t^3 + 3 \cdot t^2 + 0 \cdot t + 0 \\ - & + & +1 \cdot t^3 + 0 \cdot t^2 + 0 \cdot t + 0 \\ - \end{matrix} -\]

And that, we can view as a series of four matrix operations:

-\[ - \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-1 \\ 3 \\ -3 \\ 1\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}3 \\ -6 \\ 3 \\ 0\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-3 \\ 3 \\ 0 \\ 0\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}1 \\ 0 \\ 0 \\ 0\end{bmatrix} -\]

If we compact this into a single matrix operation, we get:

-\[ - \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix} - -1 & 3 & -3 & 1 \\ - 3 & -6 & 3 & 0 \\ - -3 & 3 & 0 & 0 \\ - 1 & 0 & 0 & 0 - \end{bmatrix} -\]

This kind of polynomial basis representation is generally written with the bases in increasing order, which means we need to flip our t matrix horizontally, and our big "mixing" matrix upside down:

-\[ - \begin{bmatrix}1 & t & t^2 & t^3\end{bmatrix} \cdot \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} -\]

And then finally, we can add in our original coordinates as a single third matrix:

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

We can perform the same trick for the quadratic curve, in which case we end up with:

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

If we plug in a t value, and then multiply the matrices, we will get exactly the same values as when we evaluate the original polynomial function, or as when we evaluate the curve using progessive linear interpolation.

-

-So: why would we bother with matrices? Matrix representations allow us to discover things about functions that would otherwise be hard to tell. It turns out that the curves form triangular matrices, and they have a determinant equal to the product of the actual coordinates we use for our curve. It's also invertible, which means there's a ton of properties that are all satisfied. Of course, the main question is: "Why is this useful to us, now?", and the answer to that is that it's not immediately useful, but you'll be seeing some instances where certain curve properties can be either computed via function manipulation, or via clever use of matrices, and sometimes the matrix approach can be (drastically) faster.

-

So for now, just remember that we can represent curves this way, and let's move on.

-
; } - - }, - "decasteljau": { - "locale": "en-GB", - "title": "de Casteljau's algorithm", - "getContent": function(handler) { return
- -

If we want to draw Bézier curves we can run through all values of t from 0 to 1 and then compute the weighted basis function, getting the x/y values we need to plot, but the more complex the curve gets, the more expensive this becomes. Instead, we can use "de Casteljau's algorithm" to draw curves, which is a geometric approach to drawing curves, and really easy to implement. So easy, in fact, you can do it by hand with a pencil and ruler.

-

Rather than using our calculus function to find x/y values for t, let's do this instead:

-
    -
  • treat t as a ratio (which it is). t=0 is 0% along a line, t=1 is 100% along a line.
  • -
  • Take all lines between the curve's defining points. For an order n curve, that's n lines.
  • -
  • Place markers along each of these line, at distance t. So if t is 0.2, place the mark at 20% from the start, 80% from the end.
  • -
  • Now form lines between those points. This gives n-1 lines.
  • -
  • Place markers along each of these line at distance t.
  • -
  • Form lines between those points. This'll be n-2 lines.
  • -
  • place markers, form lines, place markers, etc.
  • -
  • repeat this until you have only one line left. The point t on that line coincides with the original curve point at t.
  • -
- -
-

How to implement de Casteljau's algorithm

-

Let's just use the algorithm we just specified, and implement that:

-
function drawCurve(points[], t):
-  if(points.length==1):
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      newpoints[i] = (1-t) * points[i] + t * points[i+1]
-    drawCurve(newpoints, t)
-
-

And done, that's the algorithm implemented. Except usually you don't get the luxury of overloading the "+" operator, so let's also give the code for when you need to work with x and y values:

-
function drawCurve(points[], t):
-  if(points.length==1):
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      x = (1-t) * points[i].x + t * points[i+1].x
-      y = (1-t) * points[i].y + t * points[i+1].y
-      newpoints[i] = new point(x,y)
-    drawCurve(newpoints, t)
-
-

So what does this do? This draws a point, if the passed list of points is only 1 point long. Otherwise it will create a new list of points that sit at the t ratios (i.e. the "markers" outlined in the above algorithm), and then call the draw function for this new list.

-
-

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.

- -
; } - - }, - "flattening": { - "locale": "en-GB", - "title": "Simplified drawing", - "getContent": function(handler) { return
- -

We can also simplify the drawing process by "sampling" the curve at certain points, and then joining those points up with straight lines, a process known as "flattening", as we are reducing a curve to a simple sequence of straight, "flat" lines.

-

We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.

- - -

Try clicking on the sketch and using your up and down arrow keys to lower the number of segments for both the quadratic and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.

- -
-

How to implement curve flattening

-

Let's just use the algorithm we just specified, and implement that:

-
function flattenCurve(curve, segmentCount):
-  step = 1/segmentCount;
-  coordinates = [curve.getXValue(0), curve.getYValue(0)]
-  for(i=1; i <= segmentCount; i++):
-    t = i*step;
-    coordinates.push[curve.getXValue(t), curve.getYValue(t)]
-  return coordinates;
-
-

And done, that's the algorithm implemented. That just leaves drawing the resulting "curve" as a sequence of lines:

-
function drawFlattenedCurve(curve, segmentCount):
-  coordinates = flattenCurve(curve, segmentCount)
-  coord = coordinates[0], _coords;
-  for(i=1; i < coordinates.length; i++):
-    _coords = coordinates[i]
-    line(coords, _coords)
-    coords = _coords
-
-

We start with the first coordinate as reference point, and then just draw lines between each point and its next point.

-
-
; } - - }, - "splitting": { - "locale": "en-GB", - "title": "Splitting curves", - "getContent": function(handler) { return
- -

With de Casteljau's algorithm we 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.

- -
-

implementing curve splitting

-

We can implement curve splitting by bolting some extra logging onto the de Casteljau function:

-
left=[]
-right=[]
-function drawCurve(points[], t):
-  if(points.length==1):
-    left.add(points[0])
-    right.add(points[0])
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      if(i==0):
-        left.add(points[i])
-      if(i==newpoints.length-1):
-        right.add(points[i+1])
-      newpoints[i] = (1-t) * points[i] + t * points[i+1]
-    drawCurve(newpoints, t)
-
-

After running this function for some value t, the left and right arrays will contain all the coordinates for two new curves - one to the "left" of our t value, the other on the "right", of the same order as the original curve, and overlayed exactly on the original curve.

-
-

This is best illustrated with an animated graphic (click to play/pause):

- -
; } - - }, - "matrixsplit": { - "locale": "en-GB", - "title": "Splitting curves using matrices", - "getContent": function(handler) { return
- -

Another way to split curves is to exploit the matrix representation of a Bézier curve. In the section on matrices we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic, and cubic curves, respectively (using the reversed Bézier coefficients vector for legibility):

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

and

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0\\ - -3 & 3 & 0 & 0\\ - 3 & -6 & 3 & 0\\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

Let's say we want to split the curve at some point t = z, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we split out the the actual "point on the curve" information as a new matrix multiplication:

-\[ - B(t) = - \begin{bmatrix} - 1 & (z \cdot t) & (z \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

and

-\[ - B(t) = - \begin{bmatrix} - 1 & (z \cdot t) & (z \cdot t)^2 & (z \cdot t)^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0\\ - 0 & z & 0 & 0\\ - 0 & 0 & z^2 & 0\\ - 0 & 0 & 0 & z^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

If we could compact these matrices back to a form [t values] · [bezier matrix] · [column matrix], with the first two staying the same, then that column matrix on the right would be the coordinates of a new Bézier curve that describes the first segment, from t = 0 to t = z. As it turns out, we can do this quite easily, by exploiting some simple rules of linear algebra (and if you don't care about the derivations, just skip to the end of the box for the results!).

- -
-

Deriving new hull coordinates

-

Deriving the two segments upon splitting a curve takes a few steps, and the higher the curve order, the more work it is, so let's look at the quadratic curve first:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \underset{we\ turn\ this...}{\underbrace{\kern 2.25em Z \cdot M \kern 2.25em}} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \underset{...into\ this...}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \underset{...to\ get\ this!}{\underbrace{ \kern 1.25em \cdot \kern 1.25em Q \kern 1.25em \cdot \kern 1.25em}} - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

We do this, because [M · M-1 -] is the identity matrix (a bit like multiplying something by x/x in calculus. It doesn't do anything to the function, but it does allow you to rewrite it to something that may be easier to work with, or can be broken up differently). Adding that as matrix multiplication has no effect on the total formula, but it does allow us to change the matrix sequence [something · M] to a sequence [M · something], and that makes a world of difference: if we know what [M-1 · Z · M] is, we can apply that to our coordinates, and be left with a proper matrix representation of a quadratic Bézier curve (which is [T · M · P]), with a new set of coordinates that represent the curve from t = 0 to t = z. So let's get computing:

-\[ - Q = M^{-1} \cdot Z \cdot M = - \begin{bmatrix} - 1 & 0 & 0 \\ - 1 & \frac{1}{2} & 0 \\ - 1 & 1 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - = - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} -\]

Excellent! Now we can form our new quadratic curve:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot M \cdot Q \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \cdot - \left ( - Q - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \left ( - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1 - \end{bmatrix} -\]

- -Brilliant -: if we want a subcurve from t = 0 to t = z, we can keep the first coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the start point, and the new end point is a mixture that looks oddly similar to a bernstein polynomial of degree two, except it uses (z-1) rather than (1-z)... These new coordinates are actually really easy to compute directly!

-

Of course, that's only one of the two curves. Getting the section from t = z to t = 1 requires doing this again. We first observe what what we just did is actually evaluate the general interval [0,z], which we wrote down simplified becuase of that zero, but we actually evaluated this:

-\[ - B(t) = - \begin{bmatrix} - 1 & ( 0 + z \cdot t) & ( 0 + z \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

If we want the interval [z,1], we will be evaluating this instead:

-\[ - B(t) = - \begin{bmatrix} - 1 & ( z + (1-z) \cdot t) & ( z + (1-z) \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & z & z^2 \\ - 0 & 1-z & 2 \cdot z \cdot (1-z) \\ - 0 & 0 & (1-z)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

We're going to do the same trick, to turn [something · M] into [M · something]:

-\[ - Q' = M^{-1} \cdot Z' \cdot M = - \begin{bmatrix} - 1 & 0 & 0 \\ - 1 & \frac{1}{2} & 0 \\ - 1 & 1 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & z & z^2 \\ - 0 & 1-z & 2 \cdot z \cdot (1-z) \\ - 0 & 0 & (1-z)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - = - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} -\]

So, our final second curve looks like:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot M \cdot Q \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \cdot - \left ( - Q' - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \left ( - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z \cdot P_3 - (z-1) \cdot P_2 \\ - P_3 - \end{bmatrix} -\]

- -Nice -: we see the same as before; can keep the last coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the end point, and the new start point is a mixture that looks oddly similar to a bernstein polynomial of degree two, except it uses (z-1) rather than (1-z). These new coordinates are also really easy to compute directly!

-
-

So, using linear algebra rather than de Casteljau's algorithm, we have determined that for any quadratic curve split at some value t = z, we get two subcurves that are described as Bézier curves with simple-to-derive coordinates.

-\[ - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1 - \end{bmatrix} -\]

and

-\[ - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z \cdot P_3 - (z-1) \cdot P_2 \\ - P_3 - \end{bmatrix} -\]

We can do the same for cubic curves. However, I'll spare you the actual derivation (don't let that stop you from writing that out yourself, though) and simply show you the resulting new coordinate sets:

-\[ - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -(z-1) & z & 0 & 0 \\ - (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 & 0 \\ - -(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 - \end{bmatrix} -\]

and

-\[ - \begin{bmatrix} - -(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1)^3 \cdot z^2 & z^3 \\ - 0 & (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 \\ - 0 & 0 & -(z-1) & z \\ - 0 & 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \\ - z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \\ - z \cdot P_4 - (z-1) \cdot P_3 \\ - P_4 - \end{bmatrix} -\]

So, looking at our matrices, did we really need to compute the second segment matrix? No, we didn't. Actually having one segment's matrix means we implicitly have the other: push the values of each row in the matrix -Q - to the right, with zeroes getting pushed off the right edge and appearing back on the left, and then flip the matrix vertically. Presto, you just "calculated" -Q' -.

-

Implementing curve splitting this way requires less recursion, and is just straight arithmetic with cached values, so can be cheaper on systems were recursion is expensive. If you're doing computation with devices that are good at matrix multiplication, chopping up a Bézier curve with this method will be a lot faster than applying de Casteljau.

-
; } - - }, - "reordering": { - "locale": "en-GB", - "title": "Lowering and elevating curve order", - "getContent": function(handler) { return
- -

One interesting property of Bézier curves is that an nth - order curve can always be perfectly represented by an (n+1)th - order curve, by giving the higher order curve specific control points.

-

If we have a curve with three points, then we can create a four point curve that exactly reproduce the original curve as long as we give it the same start and end points, and for its two control points we pick "1/3rd start + 2/3rd control" and "2/3rd control + 1/3rd end", and now we have exactly the same curve as before, except represented as a cubic curve, rather than a quadratic curve.

-

The general rule for raising an nth - order curve to an (n+1)th - order curve is as follows (observing that the start and end weights are the same as the start and end weights for the old curve):

-\[ - Bézier(k,t) = \sum_{i=0}^{k} - \underset{binomial\ term}{\underbrace{\binom{k}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}} - \ \cdot \ - \underset{new\ weights}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}} - \ ,\ with\ k = n+1\ and\ w_{i-1}=0\ when\ i = 0 -\]

However, this rule also has as direct consequence that you cannot generally safely lower a curve from nth - order to (n-1)th - order, because the control points cannot be "pulled apart" cleanly. We can try to, but the resulting curve will not be identical to the original, and may in fact look completely different.

-

We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch and press your up and down arrow keys to elevate or lower the curve order.

- -

There is a good, if mathematical, explanation on the matrices necessary for optimal reduction over on Sirver's Castle, which given time will find its way in a more direct description into this article.

-
; } - - }, - "derivatives": { - "locale": "en-GB", - "title": "Derivatives", - "getContent": function(handler) { return
- -

There's a number of useful things that you can do with Bézier curves based on their derivative, and one of the more amusing observations about Bézier curves is that their derivatives are, in fact, also Bézier curves. In fact, the derivation of a Bézier curve is relatively straight forward, although we do need a bit of math. First, let's look at the derivative rule for Bézier curves, which is:

-\[ - Bézier'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot Bézier(n-1,t)_i -\]

which we can also write (observing that b in this formula is the same as our w weights, and that n times a summation is the same as a summation where each term is multiplied by n) as:

-\[ - Bézier'(n,t) = \sum_{i=0}^{n-1} Bézier(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i) -\]

Or, in plain text: the derivative of an nth degree Bézier curve is an (n-1)th degree Bézier curve, with one fewer term, and new weights w'0...w'n-1 derived from the original weights as n(wi+1 - wi), so for a 3rd degree curve, with four weights, the derivative has three new weights w'0 = 3(w1-w0), w'1 = 3(w2-w1) and w'2 = 3(w3-w2).

- -
-

"Slow down, why is that true?"

-

Sometimes just being told "this is the derivative" is nice, but you might want to see why this is indeed the case. As such, let's have a look at the proof for this derivative. First off, the weights are independent of the full Bézier function, so the derivative involves only the derivative of the polynomial basis function. So, let's find that:

-\[ - B_{n,k}(t) \frac{d}{dt} = {n \choose k} t^k (1-t)^{n-k} \frac{d}{dt} -\]

Applying the product and chain rules gives us:

-\[\begin{array}{l} - ... &= {n \choose k} \left ( - k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1 - \right ) -\end{array}\]

Which is hard to work with, so let's expand that properly:

-\[\begin{array}{l} - ... &= \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} -\end{array}\]

Now, the trick is to turn this expression into something that has binomial coefficients again, so we want to end up with things that look like "x! over y!(x-y)!". If we can do that in a way that involves terms of n-1 and k-1, we'll be on the right track.

-\[\begin{array}{l} - ... &= \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\ - - ... &= n \left ( - \frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k} - \right ) \\ - - ... &= n \left ( - \frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{(k-1)} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k} - \right ) -\end{array}\]

And that's the first part done: the two components inside the parentheses are actually regular, lower order Bezier expressions:

-\[\begin{array}{l} - ... &= n \left ( - \frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k} - \right ) - \ ,\ with\ x=n-1,\ y=k-1 - \\ - - - ... &= n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right ) -\end{array}\]

Now to apply this to our weighted Bezier curves. We'll write out the plain curve formula that we saw earlier, and then work our way through to its derivative:

-\[\begin{array}{l} - Bézier_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + ... \\ - Bézier_{n,k}(t) \frac{d}{dt} &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + \\ - & & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + \\ - & & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + \\ - & & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + \\ - & & ... -\end{array}\]

If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for k we see the following:

-\[\begin{array}{l} - n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\ - n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\ - n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\ - n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\ - ... &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\ - ... & & & -\end{array}\]

Two of these terms fall way: the first term falls away because there is no -1st term in a summation. As such, it always contributes "nothing", so we can safely completely ignore it for the purpose of finding the derivative function. The other term is the very last term in this expansion: one involving Bn-1,n -. This term would have a binomial coefficient of [i choose i+1], which is a non-existent binomial coefficient. Again, this term would contribute "nothing", so we can ignore it, too. This means we're left with:

-\[\begin{array}{l} - n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\ - n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& \ n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\ - n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\ - ... -\end{array}\]

And that's just a summation of lower order curves:

-\[ - Bézier_{n,k}(t) \frac{d}{dt} = n \cdot B_{(n-1),BLUE[0]}(t) \cdot (w_1 - w_0) - + n \cdot B_{(n-1),RED[1]}(t) \cdot (w_2 - w_1) - + n \cdot B_{(n-1),MAGENTA[2]}(t) \cdot (w_3 - w_2) - \ + \ ... -\]

We can rewrite this as a normal summation, and we're done:

-\[ - Bézier_{n,k}(t) \frac{d}{dt} = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k) - = \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underset{derivative\ weights} - {\underbrace{n \cdot (w_{k+1} - w_k)}} -\]
-

Let's rewrite that in a form similar to our original formula, so we can see the difference. We will first list our original formula for Bézier curves, and then the derivative:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} - \cdot\ - \underset{weight}{\underbrace{w_i}} -\]\[ - Bézier'(n,t) = \sum_{i=0}^{k} - \underset{binomial\ term}{\underbrace{\binom{k}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}} - \cdot\ - \underset{derivative\ weight}{\underbrace{n \cdot (w_{i+1} - w_i)}} - {\ , \ with \ k=n-1} -\]

What are the differences? In terms of the actual Bézier curve, virtually nothing! We lowered the order (rather than n, it's now n-1), but it's still the same Bézier function. The only real difference is in how the weights change when we derive the curve's function. If we have four points A, B, C, and D, then the derivative will have three points, the second derivative two, and the third derivative one:

-\[ \begin{array}{l} - B(n,t), & & w = \{A,B,C,D\} \\ - B'(n,t), & n = 3, & w' = \{A',B',C'\} &= \{3 \cdot (B-A), {\ } 3 \cdot (C-B), {\ } 3 \cdot (D-C)\} \\ - B''(n,t), & n = 2, & w'' = \{A'',B''\} &= \{2 \cdot (B'-A'), {\ } 2 \cdot (C'-B')\} \\ - B'''(n,t), & n = 1, & w''' = \{A'''\} &= \{1 \cdot (B''-A'')\} -\end{array} \]

We can keep performing this trick for as long as we have more than one weight. Once we have one weight left, the next step will see k = 0, and the result of our "Bézier function" summation is zero, because we're not adding anything at all. As such, a quadratic curve has no second derivative, a cubic curve has no third derivative, and generalized: an nth - order curve has n-1 (meaningful) derivatives, with any further derivative being zero.

-
; } - - }, - "pointvectors": { - "locale": "en-GB", - "title": "Tangents and normals", - "getContent": function(handler) { return
- -

If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested in are the tangent vector and normal vector for curve points. These are actually really easy to find. For moving, and orienting, along a curve we use the tangent, which indicates the direction travel at specific points, and is literally just the first derivative of our curve:

-\[ -\left \{ \begin{matrix} - tangent_x(t) = B'_x(t) \\ - tangent_y(t) = B'_y(t) -\end{matrix} \right. -\]

This gives us the directional vector we want. We can normalize it to give us uniform directional vectors (having a length of 1.0) at each point, and then do whatever it is we want to do based on those directions:

-\[ - d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2} -\]\[ -\left \{ \begin{matrix} - \hat{x}(t) = || tangent_x(t) || - =\frac{tangent_x(t)}{ || tangent(t) || } - = \frac{B'_x(t)}{d} \\ - \hat{y}(t) = || tangent_y(t) || - = \frac{tangent_y(t)}{ || tangent(t) || } - = \frac{B'_y(t)}{d} -\end{matrix} \right. -\]

The tangent is very useful for moving along a line, but what if we want to move away from the curve instead, perpendicular to the curve at some point t? In that case we want the "normal" vector. This vector runs at a right angle to the direction of the curve, and is typically of length 1.0, so all we have to do is rotate the normalized directional vector and we're done:

-\[ -\left \{ \begin{array}{l} - normal_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\ - normal_y(t) = \underset{quarter\ circle\ rotation} {\underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }} = \hat{x}(t) -\end{array} \right. -\] -
-

Rotating coordinates is actually very easy, if you know the rule for it. You might find it explained as "applying a rotation matrix, which is what we'll look at here, too. Essentially, the idea is to take the circles over which we can rotate, and simply "sliding the coordinates" over these circles by the desired -angle. If we want a quarter circle turn, we take the coordinate, slide it along the cirle by a quarter turn, and done.

-

To turn any point (x,y) into a rotated point (x',y') (over 0,0) by some angle φ, we apply this nicely easy computation:

-\[\begin{array}{l} - x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\ - y' = x \cdot \sin(\phi) + y \cdot \cos(\phi) -\end{array}\]

Which is the "long" version of the following matrix transformation:

-\[ - \begin{bmatrix} - x' \\ y' - \end{bmatrix} - = - \begin{bmatrix} - \cos(\phi) & -\sin(\phi) \\ - \sin(\phi) & \cos(\phi) - \end{bmatrix} - \begin{bmatrix} - x \\ y - \end{bmatrix} -\]

And that's all we need to rotate any coordinate. Note that for quarter, half and three quarter turns these functions become even easier, since sin and cos for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.

-

But -why - does this work? Why this matrix multiplication? Wikipedia (Technically, Thomas Herter and Klaus Lott) tells us that a rotation matrix can be -treated as a sequence of three (elementary) shear operations. When we combine this into a single matrix operation (because all matrix multiplications can be collapsed), we get the matrix that you see above. DataGenetics have an excellent article about this very thing: it's really quite cool, and I strongly recommend taking a quick break from this primer to read that article.

-
-

The following two graphics show the tangent and normal along a quadratic and cubic curve, with the direction vector coloured blue, and the normal vector coloured red (the markers are spaced out evenly as t-intervals, not spaced equidistant).

- -
- - -
-
; } - - }, - "components": { - "locale": "en-GB", - "title": "Component functions", - "getContent": function(handler) { return
- -

One of the first things people run into when they start using Bézier curves in their own programs is "I know how to draw the curve, but how do I determine the bounding box?". It's actually reasonably straight forward to do so, but it requires having some knowledge on exploiting math to get the values we need. For bounding boxes, we aren't actually interested in the curve itself, but only in its "extremities": the minimum and maximum values the curve has for its x- and y-axis values. If you remember your calculus (provided you ever took calculus, otherwise it's going to be hard to remember) we can determine function extremities using the first derivative of that function, but this poses a problem, since our function is parametric: every axis has its own function.

-

The solution: compute the derivative for each axis separately, and then fit them back together in the same way we do for the original.

-

Let's look at how a parametric Bézier curve "splits up" into two normal functions, one for the x-axis and one for the y-axis. Note the left-most figure is again an interactive curve, without labeled axes (you get coordinates in the graph instead). The center and right-most figures are the component functions for computing the x-axis value, given a value for t (between 0 and 1 inclusive), and the y-axis value, respectively.

-

If you move points in a curve sideways, you should only see the middle graph change; likely, moving points vertically should only show a change in the right graph.

- - -
; } - - }, - "extremities": { - "locale": "en-GB", - "title": "Finding extremities: root finding", - "getContent": function(handler) { return
- -

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

-

Quadratic curves: linear derivatives.

-

Finding the solution for "where is this line 0" should be trivial:

-\[ -\begin{align} - l(x) = ax + b &= 0,\\ - ax + b &= 0,\\ - ax &= -b \\ - x &= \frac{-b}{a} -\end{align} -\]

Done. And quadratic curves have no meaningful second derivative, so we're really done.

-

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

-

First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the derivatives section:

-\[ -\begin{align} - B(t)\ uses\ \{ p_1,p_2,p_3,p_4 \} \\ - B'(t)\ uses\ \{ v_1.v_2,v_3 \},\ where\ v_1 = 3(p_2-p_1),\ v_2 = 3(p_3-p_2),\ v_3 = 3(p_4-p_3) -\end{align} -\]

And then, using these v values, we can find out what our a, b, and c should be:

-\[ -\begin{align} - B'(t) &= v_1(1-t)^2 + 2v_2(1-t)t + v_3t^2 \\ - ... &= v_1(t^2 - 2t + 1) + 2v_2(t-t^2) + v_3t^2 \\ - ... &= v_1t^2 - 2v_1t + v_1 + 2v_2t - 2v_2t^2 + v_3t^2 \\ - ... &= v_1t^2 - 2v_2t^2 + v_3t^2 - 2v_1t + v_1 + 2v_2t \\ - ... &= (v_1-2v_2+v_3)t^2 + 2(v_2-v_1)t + v_1 -\end{align} -\]

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:

-\[ -\begin{align} - a &= v_1-2v_2+v_3 = 3(-p_1 + 3p_2 - 3p_3 + p_4) \\ - b &= 2(v_2-v_1) = 6(p_1 - 2p_2 + p_3) \\ - c &= v_1 = 3(p_2-p_1) -\end{align} -\]

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.

-

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

-\[ - very\ hard:\ solve\ at^3 + bt^2 + ct + d = 0\\ - easier:\ solve\ t^3 + pt + q = 0 -\]

This is easier because for the "easier formula" we can 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).

-

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

- -
-

Implementing Cardano's algorithm for finding all real roots

-

The "real roots" part is fairly important, because while you cannot take a square, cube, etc. root of a negative number in the "real" number space (denoted with ℝ), this is perfectly fine in the "complex" number space (denoted with ℂ). And, as it so happens, Cardano is also attributed as the first mathematician in history to have made use of complex numbers in his calculations. For this very algorithm!

-
// A helper function to filter for values in the [0,1] interval:
-function accept(t) {
-  return 0<=t && t <=1;
-}
-
-// A real-cuberoots-only function:
-function crt(v) {
-  if(v<0) return -Math.pow(-v,1/3);
-  return Math.pow(v,1/3);
-}
-
-// Now then: given cubic coordinates {pa, pb, pc, pd} find all roots.
-function getCubicRoots(pa, pb, pc, pd) {
-  var d = (-pa + 3*pb - 3*pc + pd),
-  a = (3*pa - 6*pb + 3*pc) / d,
-  b = (-3*pa + 3*pb) / d,
-  c = pa / d;
-
-  var p = (3*b - a*a)/3,
-  p3 = p/3,
-  q = (2*a*a*a - 9*a*b + 27*c)/27,
-  q2 = q/2,
-  discriminant = q2*q2 + p3*p3*p3;
-
-  // and some variables we're going to use later on:
-  var u1,v1,root1,root2,root3;
-
-  // three possible real roots:
-  if (discriminant < 0) {
-    var mp3  = -p/3,
-    mp33 = mp3*mp3*mp3,
-    r    = sqrt( mp33 ),
-    t    = -q / (2*r),
-    cosphi = t<-1 ? -1 : t>1 ? 1 : t,
-    phi  = acos(cosphi),
-    crtr = cuberoot(r),
-    t1   = 2*crtr;
-    root1 = t1 * cos(phi/3) - a/3;
-    root2 = t1 * cos((phi+2*pi)/3) - a/3;
-    root3 = t1 * cos((phi+4*pi)/3) - a/3;
-    return [root1, root2, root3].filter(accept);
-  }
-
-  // three real roots, but two of them are equal:
-  if(discriminant === 0) {
-    u1 = q2 < 0 ? cuberoot(-q2) : -cuberoot(q2);
-    root1 = 2*u1 - a/3;
-    root2 = -u1 - a/3;
-    return [root1, root2].filter(accept);
-  }
-
-  // one real root, two complex roots
-  var sd = sqrt(discriminant);
-  u1 = cuberoot(sd - q2);
-  v1 = cuberoot(sd + q2);
-  root1 = u1 - v1 - a/3;
-  return [root1].filter(accept);
-}
-
-
-

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.

-

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.

-

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 root finding method (yes, after -that - Newton), which we can make use of.

-

The Newton-Raphson approach consists of picking a value t (any will do), and getting the corresponding value 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:

-\[ - t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_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)

-

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?

-

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.

-

In conclusion:

-

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:

- - -
; } - - }, - "boundingbox": { - "locale": "en-GB", - "title": "Bounding boxes", - "getContent": function(handler) { return
- -

If we have the extremities, and the start/end points, a simple for loop that tests for min/max values for x and y means we have the four values we need to box in our curve:

-

-Computing the bounding box for a Bézier curve:

-
    -
  1. Find all t value(s) for the curve derivative's x- and y-roots.
  2. -
  3. Discard any t value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].
  4. -
  5. Determine the lowest and highest value when plugging the values t=0, t=1 and each of the found roots into the original functions: the lowest value is the lower bound, and the highest value is the upper bound for the bounding box we want to construct.
  6. -
-

Applying this approach to our previous root finding, we get the following bounding boxes (with all curve extremity points shown on the curve):

- - -

We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first need to look at how aligning works.

-
; } - - }, - "aligning": { - "locale": "en-GB", - "title": "Aligning curves", - "getContent": function(handler) { return
- -

While there are an incredible number of curves we can define by varying the x- and y-coordinates for the control points, not all curves are actually distinct. For instance, if we define a curve, and then rotate it 90 degrees, it's still the same curve, and we'll find its extremities in the same spots, just at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to "axis-align" it.

-

Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first point lies on (0,0), which turns our n term polynomial functions into n-1 term functions. The order stays the same, but we have less terms. Then, we can rotate the curves so that the last point always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the function for the y-component to an n-2 term function. For instance, if we have a cubic curve such as this:

-\[ -\left \{ \begin{matrix} - x = BLUE[120] \cdot (1-t)^3 BLUE[+ 35] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 220] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 220] \cdot t^3 \\ - y = BLUE[160] \cdot (1-t)^3 BLUE[+ 200] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 260] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 40] \cdot t^3 -\end{matrix} \right. -\]

Then translating it so that the first coordinate lies on (0,0), moving all x coordinates by -120, and all y coordinates by -160, gives us:

-\[ -\left \{ \begin{matrix} - x = BLUE[0] \cdot (1-t)^3 BLUE[- 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 100] \cdot t^3 \\ - y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 120] \cdot t^3 -\end{matrix} \right. -\]

If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:

-\[ -\left \{ \begin{matrix} - x = BLUE[0] \cdot (1-t)^3 BLUE[+ 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 12] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\ - y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 140] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 0] \cdot t^3 -\end{matrix} \right. -\]

If we drop all the zero-terms, this gives us:

-\[ -\left \{ \begin{array}{l} - x = BLUE[85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 13] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\ - y = BLUE[40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 141] \cdot 3 \cdot (1-t) \cdot t^2 -\end{array} \right. -\]

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:

- - -
; } - - }, - "tightbounds": { - "locale": "en-GB", - "title": "Tight boxes", - "getContent": function(handler) { return
- -

With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for curves. We first align our curve, recording the translation we performed, "T", and the rotation angle we used, "R". We then determine the aligned curve's normal bounding box. Once we have that, we can map that bounding box back to our original curve by rotating it by -R, and then translating it by -T. We now have nice tight bounding boxes for our curves:

- - -

These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding precision is often not worth it. If there is high demand for it, I'll add a section on how to precisely compute the best fit bounding box, but the maths is fairly gruelling and just not really worth spending time on.

-
; } - - }, - "inflections": { - "locale": "en-GB", - "title": "Curve inflections", - "getContent": function(handler) { return
- -

Now that we know how to align a curve, there's one more thing we can calculate: inflection points. Imagine we have a variable size circle that we can slide up against our curve. We place it against the curve and adjust its radius so that where it touches the curve, the curvatures of the curve and the circle are the same, and then we start to slide the circle along the curve - for quadratic curves, we can always do this without the circle behaving oddly: we might have to change the radius of the circle as we slide it along, but it'll always sit against the same side of the curve.

-

But what happens with cubic curves? Imagine we have an S curve and we place our circle at the start of the curve, and start sliding it along. For a while we can simply adjust the radius and things will be fine, but once we get to the midpoint of that S, something odd happens: the circle "flips" from one side of the curve to the other side, in order for the curvatures to keep matching. This is called an inflection, and we can find out where those happen relatively easily.

-

What we need to do is solve a simple equation:

-\[ - C(t) = 0 -\]

What we're saying here is that given the curvature function C(t), we want to know for which values of t this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does C(t) look like? Actually something that seems not too hard:

-\[ - C(t) = Bézier_x\prime(t) \cdot Bézier_y{\prime\prime}(t) - Bézier_y\prime(t) \cdot Bézier_x{\prime\prime}(t) -\]

So the function C(t) is wholly defined by the first and second derivative functions for the parametric dimensions of our curve. And as already shown, derivatives of Bézier curves are just simpler Bézier curves, with very easy to compute new coefficients, so this should be pretty easy.

-

However as we've seen in the section on aligning, aligning lets us simplify things a lot, by completely removing the contributions of the first coordinate from most mathematical evaluations, and removing the last y coordinate as well by virtue of the last point lying on the x-axis. So, while we can evaluate C(t) = 0 for our curve, it'll be much easier to first axis-align the curve and then evalutating the curvature function.

- -
-

Let's derive the full formula anyway

-

Of course, before we do our aligned check, let's see what happens if we compute the curvature function without axis-aligning. We start with the first and second derivatives, given our basis functions:

-\[ -\begin{align*} - & Bézier(t) = x_1(1-t)^3 + 3x_2(1-t)^2t + 3x_3(1-t)t^2 + x_4t^3 \\ - & Bézier^\prime(t) = a(1-t)^2 + 2b(1-t)^t + ct^2\ \left\{ a=3(x_2-x_1),b=3(x_3-x_2),c=3(x_4-x_3) \right\} \\ - & Bézier^{\prime\prime}(t) = u(1-t) + vt\ \left\{ u=2(b-a),v=2(c-b) \right\}\ -\end{align*} -\]

And of course the same functions for y:

-\[ -\begin{align*} - & Bézier(t) = y_1(1-t)^3 + 3y_2(1-t)^2t + 3y_3(1-t)t^2 + y_4t^3 \\ - & Bézier^\prime(t) = d(1-t)^2 + 2e(1-t)^t + ft^2\\ - & Bézier^{\prime\prime}(t) = w(1-t) + zt -\end{align*} -\]

Asking a computer to now compose the C(t) function for us (and to expand it to a readable form of simple terms) gives us this rather overly complicated set of arithmetic expressions:

-\[ -\begin{array} - -18 t^2 x_2 y_1+36 t^2 x_3 y_1-18 t^2 x_4 y_1+18 t^2 x_1 y_2-54 t^2 x_3 y_2 \\ - +36 t^2 x_4 y_2-36 t^2 x_1 y_3+54 t^2 x_2 y_3-18 t^2 x_4 y_3+18 t^2 x_1 y_4 \\ - -36 t^2 x_2 y_4+18 t^2 x_3 y_4+36 t x_2 y_1-54 t x_3 y_1+18 t x_4 y_1-36 t x_1 y_2 \\ - +54 t x_3 y_2-18 t x_4 y_2+54 t x_1 y_3-54 t x_2 y_3-18 t x_1 y_4+18 t x_2 y_4 \\ - -18 x_2 y_1+18 x_3 y_1+18 x_1 y_2-18 x_3 y_2-18 x_1 y_3+18 x_2 y_3 -\end{array} -\]

That is... unwieldy. So, we note that there are a lot of terms that involve multiplications involving x1, y1, and y4, which would all disappear if we axis-align our curve, which is why aligning is a great idea.

-
-

Aligning our curve so that three of the eight coefficients become zero, we end up with the following simple term function for C(t):

-\[ - 18 \left ( (3 x_3 y_2+2 x_4 y_2+3 x_2 y_3-x_4 y_3)t^2 + (3 x_3 y_2-x_4 y_2-3 x_2 y_3)t + (x_2 y_3-x_3 y_2) \right ) -\]

That's a lot easier to work with: we see a fair number of terms that we can compute and then cache, giving us the following simplification:

-\[ - \left.\begin{matrix} - a = x_3 \cdot y_2 \\ - b = x_4 \cdot y_2 \\ - c = x_2 \cdot y_3 \\ - d = x_4 \cdot y_3 - \end{matrix}\right\} - \ C(t) = 18 \cdot \left ( (-3a + 2b + 3c - d)t^2 + (3a - b - 3c)t + (c - a) \right ) -\]

This is a plain quadratic curve, and we know how to solve C(t) = 0; we use the quadratic formula:

-\[ - \left.\begin{matrix} - x =& 18(-3a + 2b + 3c - d) \\ - y =& 18(3a - b - 3c) \\ - z =& 18(c - a) - \end{matrix}\right\} - \ C(t) = 0 \ \Rightarrow\ t = \frac{-y \pm \sqrt{y^2 - 4 x z}}{2x} -\]

We can easily compute this value if the descriminator isn't a negative number (because we only want real roots, not complex roots), and if x is not zero, because divisions by zero are rather useless.

-

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.

- -
; } - - }, - "canonical": { - "locale": "en-GB", - "title": "Canonical form (for cubic curves)", - "getContent": function(handler) { return
- -

While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve. As a curvature controlled by more than one control points, it exhibits all kinds of features like loops, cusps, odd colinear features, and up to two inflection points because the curvature can change direction up to three times. Now, knowing what kind of curve we're dealing with means that some algorithms can be run more efficiently than if we have to implement them as generic solvers, so is there a way to determine the curve type without lots of work?

-

As it so happens, the answer is yes and the solution we're going to look at was presented by Maureen C. Stone from Xerox PARC and Tony D. deRose from the University of Washington in their joint paper "A Geometric Characterization of Parametric Cubic curves". It was published in 1989, and defines curves as having a "canonical" form (i.e. a form that all curves can be reduced to) from which we can immediately tell which features a curve will have. So how does it work?

-

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:

- -

This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0), (0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have certain features. Namely, if the fourth point is...

-
    -
  1. anywhere on and in the red zone, the curve will be self-intersecting, yielding either a cusp or a loop. Anywhere inside the the red zone, this will be a loop. We won't know where that loop is (in terms of t values), but we are guaranteed that there is one.
  2. -
  3. on the left (red) edge, the curve will have a cusp. We again don't know where, just that it -has one. This edge is described by the function:
  4. -
-\[ - y = \frac{-x^2 + 2x + 3}{4}, \{ x \leq 1 \} - \]
    -
  1. on the lower right (pink) edge, the curve will have a loop at t=1, so we know the end coordinate of -the curve also lies on the curve. This edge is described by the function:
  2. -
-\[ - y = \frac{\sqrt{3(4x - x^2)} - x}{2}, \{ 0 \leq x \leq 1 \} - \]
    -
  1. on the top (blue) edge, the curve will have a loop at t=0, so we know the start coordinate of -the curve also lies on the curve. This edge is described by the function:
  2. -
-\[ - y = \frac{-x^2 + 3x}{3}, \{ x \leq 0 \} - \]
    -
  1. inside the green zone, the curve will have a single inflection, switching concave/convex once.
  2. -
  3. between the red and green zones, the curve has two inflections, meaning its curvature switches between -concave/convex form twice.
  4. -
  5. anywhere on the right of the red zone, the curve will have no inflections. It'll just be a well-behaved arch.
  6. -
-

Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.

- -
-

Wait, where do those lines come from?

-

Without repeating the paper mentioned at the top of this section, the loop-boundaries come from rewriting the curve into canonical form, and then solving the formulae for which constraints must hold for which possible curve properties. In the paper these functions yield formulae for where you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived for the full cubic expression, meaning they apply to t=-∞ to t=∞... For Bézier curves we only care about the "clipped interval" t=0 to t=1, so some of the properties that apply when you look at the curve over an infinite interval simply don't apply to the Bézier curve interval.

-

The right bound for the loop region, indicating where the curve switches from "having inflections" to "having a loop", for the general cubic curve, is actually mirrored over x=1, but for Bézier curves this right half doesn't apply, so we don't need to pay attention to it. Similarly, the boundaries for t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general curve does over the interval t=0 to t=1.

-

For the full details, head over to the paper and read through sections 3 and 4. If you still remember your high school precalculus, you can probably follow along with this paper, although you might have to read it a few times before all the bits "click".

-
-

So now the question becomes: how do we manipulate our curve so that it fits this canonical form, with three fixed points, and one "free" point? Enter linear algerba. Don't worry, I'll be doing all the math for you, as well as show you what the effect is on our curves, but basically we're going to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus approach is very hard to work with, when the equivalent geometrical solution is super obvious.

-

The approach is going to start with a curve that doesn't have all-colinear points (so we need to make sure the points don't all fall on a straight line), and then applying four graphics operations that you will probably have heard of: translation (moving all points by some fixed x- and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an operation that turns rectangles into parallelograms).

-

Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going to make use of an interesting trick here, by pretending our 2D coordinates are 3D, with the z coordinate simply always being 1. This is an old trick in graphics to overcome the limitations of 2D transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form (ax + by, cx + dy), which means we can't do translation, since that requires we end up with some kind of (x + a, y + b). If we add a bogus z coordinate that is always 1, then we can suddenly add arbitrary values. For example:

-\[ -\left [ \begin{array} - 01 & 0 & a \\ - 0 & 1 & b \\ - 0 & 0 & 1 - \end{array} \right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - z=1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - 1 \cdot x + 0 \cdot y + a \cdot z \\ - 0 \cdot x + 1 \cdot y + b \cdot z \\ - 0 \cdot x + 0 \cdot y + 1 \cdot z - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + a \cdot 1 \\ - y + b \cdot 1 \\ - 1 \cdot z - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + a \\ - y + b \\ - z=1 - \end{matrix} -\right ] -\]

Sweet! z stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:

-\[ -T_1 = -\left [ \begin{array} - 01 & 0 & -{P_1}_x \\ - 0 & 1 & -{P_1}_y \\ - 0 & 0 & 1 - \end{array} \right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - 1 \cdot x + 0 \cdot y - {P_1}_x \cdot 1 \\ - 0 \cdot x + 1 \cdot y - {P_1}_y \cdot 1 \\ - 0 \cdot x + 0 \cdot y + 1 \cdot 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x - {P_1}_x \\ - y - {P_1}_y \\ - 1 - \end{matrix} -\right ] -\]

Running all our coordinates through this transformation gives a new set of coordinates, let's call those U, where the first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the x=0 line, so what we want is a transformation matrix that, when we run it, subtracts x from whatever x we currently have. This is called shearing, and the typical x-shear matrix and its transformation looks like this:

-\[ -\left [ - \begin{matrix} - 1 & S & 0 \\ - 0 & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + S \cdot y \\ - y \\ - 1 - \end{matrix} -\right ] -\]

So we want some shearing value that, when multiplied by y, yields -x, so our x coordinate becomes zero. That value is simpy -x/y, because -x/y * y = -x. Done:

-\[ -T_2 = -\left [ - \begin{matrix} - 1 & -\frac{ {U_2}_x }{ {U_2}_y } & 0 \\ - 0 & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on (0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to do some scaling to make sure it ends up at (0,1). Additionally, we want point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3x, and y-scaling by point2y. This is really easy:

-\[ -T_3 = -\left [ - \begin{matrix} - \frac{1}{ {V_3}_x } & 0 & 0 \\ - 0 & \frac{1}{ {V_2}_y } & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared point 2 earlier. In this case, we do the same trick, but with y/x rather than x/y because we're not x-shearing but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:

-\[ -T_4 = -\left [ - \begin{matrix} - 1 & 0 & 0 \\ - \frac{1 - {W_3}_y}{ {W_3}_x } & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:

-\[ -mapped_4 = \left ( - \begin{matrix} - x = \left ( - \frac - { - -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} - } - { - -x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2} - } - \right ) -\\ - y = \left ( - \frac{(-y_1+y_4)}{-y_1+y_2} - + - \frac - { - \left ( 1 - \frac{-y_1+y_3}{-y_1+y_2} \right ) - \left ( -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} \right ) - } - { - -x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2} - } - \right ) - \end{matrix} -\right ) -\]

That looks very complex, but notice that every coordinate value is being offset by the initial translation, and a lot of terms in there repeat: it's pretty easy to calculate this fast, since there's so much we can cache and reuse while we compute this mapped coordinate!

-

First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?

-\[ -... = \left ( - \begin{matrix} - x = \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right ) -\\ - y = - \frac{y_4}{y_2} - + - \left ( 1 - \frac{y_3}{y_2} \right ) - \cdot - \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right ) - \end{matrix} -\right ) -\]

Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:

-\[ -... = \left ( - \begin{matrix} - x = (x_4 - x_2 \cdot f_{42}) / ( x_3- x_2 \cdot f_{32} ) -\\ - y = - f_{42} - + - \left ( 1 - f_{32} \right ) - \cdot - x - \end{matrix} -\right ), f_{32} = \frac{y_3}{y_2}, f_{42} = \frac{y_4}{y_2} -\]

That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it look!

- -
-

How do you track all that?

-

Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply use Mathematica. Tracking all this math by hand is insane, and we invented computers, literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the program do the math for me. And real math, too, with symbols, not with numbers. In fact, here's the Mathematica notebook if you want to see how this works for yourself.

-

Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's $295 for home use, but it's also free when you buy a $35 raspberry pi. Obviously, I bought a raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to do, Mathematica can just do it for you. And we don't have to be geniusses to work out what the maths looks like. That's what we have computers for.

-
-

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:

- -
; } - - }, - "arclength": { - "locale": "en-GB", - "title": "Unknown title (arclength)", - "getContent": function(handler) { return
-
; } - - }, - "arclengthapprox": { - "locale": "en-GB", - "title": "Unknown title (arclengthapprox)", - "getContent": function(handler) { return
-
; } - - }, - "tracing": { - "locale": "en-GB", - "title": "Unknown title (tracing)", - "getContent": function(handler) { return
-
; } - - }, - "intersections": { - "locale": "en-GB", - "title": "Unknown title (intersections)", - "getContent": function(handler) { return
-
; } - - }, - "curveintersection": { - "locale": "en-GB", - "title": "Unknown title (curveintersection)", - "getContent": function(handler) { return
-
; } - - }, - "abc": { - "locale": "en-GB", - "title": "Unknown title (abc)", - "getContent": function(handler) { return
-
; } - - }, - "moulding": { - "locale": "en-GB", - "title": "Unknown title (moulding)", - "getContent": function(handler) { return
-
; } - - }, - "pointcurves": { - "locale": "en-GB", - "title": "Unknown title (pointcurves)", - "getContent": function(handler) { return
-
; } - - }, - "catmullconv": { - "locale": "en-GB", - "title": "Unknown title (catmullconv)", - "getContent": function(handler) { return
-
; } - - }, - "catmullmoulding": { - "locale": "en-GB", - "title": "Unknown title (catmullmoulding)", - "getContent": function(handler) { return
-
; } - - }, - "polybezier": { - "locale": "en-GB", - "title": "Unknown title (polybezier)", - "getContent": function(handler) { return
-
; } - - }, - "shapes": { - "locale": "en-GB", - "title": "Unknown title (shapes)", - "getContent": function(handler) { return
-
; } - - }, - "projections": { - "locale": "en-GB", - "title": "Unknown title (projections)", - "getContent": function(handler) { return
-
; } - - }, - "offsetting": { - "locale": "en-GB", - "title": "Unknown title (offsetting)", - "getContent": function(handler) { return
-
; } - - }, - "graduatedoffset": { - "locale": "en-GB", - "title": "Unknown title (graduatedoffset)", - "getContent": function(handler) { return
-
; } - - }, - "circles": { - "locale": "en-GB", - "title": "Unknown title (circles)", - "getContent": function(handler) { return
-
; } - - }, - "circles_cubic": { - "locale": "en-GB", - "title": "Unknown title (circles_cubic)", - "getContent": function(handler) { return
-
; } - - }, - "arcapproximation": { - "locale": "en-GB", - "title": "Unknown title (arcapproximation)", - "getContent": function(handler) { return
-
; } - - }, - "bsplines": { - "locale": "en-GB", - "title": "Unknown title (bsplines)", - "getContent": function(handler) { return
-
; } - - }, - "comments": { - "locale": "en-GB", - "title": "Unknown title (comments)", - "getContent": function(handler) { return
-
; } - - }, - "locale-switcher": { - "locale": "en-GB", - "title": "locale-switcher", - "getContent": function(handler) { return
-

Read this in your own language:

- -

Don't see your language listed? Help translate this content! -

-
; } - - } -}; diff --git a/locales/zh-CN/content.js b/locales/zh-CN/content.js deleted file mode 100644 index 8b0ecf13..00000000 --- a/locales/zh-CN/content.js +++ /dev/null @@ -1,1902 +0,0 @@ -var React = require('react'); -var Graphic = require("../../components/Graphic.jsx"); -var SectionHeader = require("../../components/SectionHeader.jsx"); - -module.exports = { - "preface": { - "locale": "zh-CN", - "title": "序言", - "getContent": function(handler) { return
- -

我们通常用线条来绘制2D图形,大致分为两种线条:直线和曲线。不论我们动手还是用电脑,都能很容易地画出第一种线条。只要给电脑起点和终点,砰!直线就画出来了。没什么好疑问的。

-

然而,绘制曲线却是个大问题。虽然我们可以很容易地徒手画出曲线,但除非给出描述曲线的数学函数,不然计算机无法画出曲线。实际上,画直线时也需要数学函数,但画直线所需的方程式很简单,我们在这里不去考虑。在计算机看来,所有线条都是“函数”,不管它们是直线还是曲线。然而,这就表示我们需要找到能在计算机上表现良好的曲线方程。这样的曲线有很多种,在本文我们主要关注一类特殊的、备受关注的函数,基本上任何画曲线的地方都会用到它:贝塞尔曲线。

-

它们是以Pierre Bézier命名的,尽管他并不是第一个,或者说唯一“发明”了这种曲线的人,但他让世界知道了这种曲线十分适合设计工作(在1962年为Renault工作并发表了他的研究)。有人也许会说数学家Paul de Casteljau是第一个发现这类曲线特性的人,在Citroën工作时,他提出了一种很优雅的方法来画这些曲线。然而,de Casteljau没有发表他的工作,这使得“谁先发现”这一问题很难有一个确切的答案。 -贝塞尔曲线本质上是伯恩斯坦多项式,这是Sergei Natanovich Bernstein研究的一种数学函数,关于它们的出版物至少可以追溯到1912年。无论如何,这些都只是一些冷知识,你可能更在意的是这些曲线很方便:你可以连接多条贝塞尔曲线,并且连接起来的曲线看起来就像是一条曲线。甚至,在你在Photoshop中画“路径”或使用一些像Flash、Illustrator和Inkscape这样的矢量绘图程序时,所画的曲线都是贝塞尔曲线。

-

那么,要是你自己想编程实现它们呢?有哪些陷阱?你怎么画它们?包围盒是怎么样的,怎么确定交点,怎么拉伸曲线,简单来说:你怎么对曲线做一切你想做的事?这就是这篇文章想说的。准备好学习一些数学吧!

-

—Pomax (推特账号, @TheRealPomax)

- -
-

注意:几乎所有的贝塞尔图形都是可交互的。

-

这个页面使用了基于Bezier.js 的可交互例子,还有一些用MathJax 排版的“真正的”数学(LaTeX形式)。这个页面是用Webpack离线生成的React应用,这便让加入“查看源码”选项更具挑战性了。我仍然试图将它们添加回来,但跟前几年的版本相比,不觉得它能够支撑部署这个更新。

-

这本书是开源的。

-

这本书是开源的软件项目,现有两个github仓库。第一个https://github.com/pomax/bezierinfo,它是你现在在看的这个,纯粹用来展示的版本。另外一个https://github.com/pomax/BezierInfo-2,是带有所有html, javascript和css的开发版本。你可以fork任意一个,随便做些什么,当然除了把它当作自己的作品来商用。 =)

-

用到的数学将有多复杂?

-

这份入门读物用到的大部分数学知识都是高中所学的。如果你理解基本的计算并能看懂英文的话,就能上手这份材料。有时候会用到复杂一点的数学,但如果你不想深究它们,可以选择跳过段落里的“详解”部分,或者直接跳到章节末尾,避开那些看起来很深入的数学。章节的末尾往往会列出一些结论,因此你可以直接利用这些结论。

-

问题,评论:

-

如果你有对于新章节的一些建议,点击 Github issue tracker (也可以点右上角的repo链接)。如果你有关于材料的一些问题,由于我现在在做改写工作,目前没有评论功能,但你可以用issue跟踪来发表评论。一旦完成重写工作,我会把评论功能加上,或者会有“选择文字段落,点击‘问题’按钮来提问”的系统。到时候我们看看。

-

给我买杯咖啡?

-

如果你很喜欢这本书,或发现它对你要做的事很有帮助,或者你想知道怎么表达自己对这本书的感激,你可以 给我买杯咖啡 ,所少钱取决于你。这份工作持续了很多年,从一份小小的简要到70多页关于贝塞尔曲线的读物,在完成它的过程中倾注了很多咖啡。我从未后悔花在这上面的每一分钟,但如果有更多咖啡的话,我可以坚持写下去!

-
-
; } - - }, - "introduction": { - "locale": "zh-CN", - "title": "简单介绍", - "getContent": function(handler) { return
- -

让我们有个好的开始:当我们在谈论贝塞尔曲线的时候,所指的就是你在如下图像看到的东西。它们从某些起点开始,到终点结束,并且受到一个或多个的“中间”控制点的影响。本页面上的图形都是可交互的,你可以拖动这些点,看看这些形状在你的操作下会怎么变化。

- -
- - -
-

这些曲线在计算机辅助设计和计算机辅助制造应用(CAD/CAM)中用的很多。在图形设计软件中也常用到,像Adobe Illustrator, Photoshop, Inkscape, Gimp等等。还可以应用在一些图形技术中,像矢量图形(SVG)和OpenType字体(ttf/otf)。许多东西都用到贝塞尔曲线,如果你想更了解它们...准备好继续往下学吧!

-
; } - - }, - "whatis": { - "locale": "zh-CN", - "title": "什么构成了贝塞尔曲线?", - "getContent": function(handler) { return
- -

操作点的移动,看看曲线的变化,可能让你感受到了贝塞尔曲线是如何表现的。但贝塞尔曲线究竟什么呢?有两种方式来解释贝塞尔曲线,并且可以证明它们完全相等,但是其中一种用到了复杂的数学,另外一种比较简单。所以...我们先从简单的开始吧:

-

贝塞尔曲线是线性插值的结果。这听起来很复杂,但你在很小的时候就做过线性插值:当你指向两个物体中的另外一个物体时,你就用到了线性插值。它就是很简单的“选出两点之间的一个点”。

-

如果我们知道两点之间的距离,并想找出离第一个点20%间距的一个新的点(也就是离第二个点80%的间距),我们可以通过简单的计算来得到:

-\[ -Given \left ( - \begin{align} - p_1 &= some\ point \\ - p_2 &= some\ other\ point \\ - distance &= (p_2 - p_1) \\ - ratio &= \frac{percentage}{100} \\ - \end{align} -\right ),\ our\ new\ point = p_1 + distance \cdot ratio -\]

让我们来通过实际操作看一下:下面的图形都是可交互的,因此你可以通过上下键来增加或减少插值距离,来观察图形的变化。我们从三个点构成的两条线段开始。通过对各条线段进行线性插值得到两个点,对点之间的线段再进行线性插值,产生一个新的点。最终这些点——所有的点都可以通过选取不同的距离插值产生——构成了贝塞尔曲线:

- -

这为我们引出了复杂的数学:微积分。

-

虽然我们刚才好像没有用到这个,我们实际上只是逐步地画了一条二次曲线,而不是一次画好。贝塞尔曲线的一个很棒的特性就是它们可以通过多项式方程表示,也可以用很简单的插值形式表示。因此,反过来说,我们可以基于“真正的数学”(检查方程式,导数之类的东西),也可以通过观察曲线的“机械”构成(比如说,可以得知曲线永远不会延伸超过我们用来构造它的点),来看看这些曲线能够做什么。

-

让我们从更深的层次来观察贝塞尔曲线。看看它们的数学表达式,从这些表达式衍生得到的属性,以及我们可以对贝塞尔曲线做的事。

-
; } - - }, - "explanation": { - "locale": "zh-CN", - "title": "贝塞尔曲线的数学原理", - "getContent": function(handler) { return
- -

贝塞尔曲线是“参数”方程的一种形式。从数学上讲,参数方程作弊了:“方程”实际上是一个从输入到唯一输出的、良好定义的映射关系。几个输入进来,一个输出返回。改变输入变量,还是只有一个输出值。参数方程在这里作弊了。它们基本上干了这么件事,“好吧,我们想要更多的输出值,所以我们用了多个方程”。举个例子:假如我们有一个方程,通过一些计算,将假设为x的一些值映射到另外的值:

-\[ - f(x) = \cos(x) -\]

记号f(x)是表示函数的标准方式(为了方便起见,如果只有一个的话,我们称函数为f),函数的输出根据一个变量(本例中是x)变化。改变xf(x)的输出值也会变。

-

到目前没什么问题。现在,让我们来看一下参数方程,以及它们是怎么作弊的。我们取以下两个方程:

-\[ -\begin{matrix} - f(a) = \cos(a) \\ - f(b) = \sin(b) -\end{matrix} -\]

这俩方程没什么让人印象深刻的,只不过是正弦函数和余弦函数,但正如你所见,输入变量有两个不同的名字。如果我们改变了a的值,f(b)的输出不会有变化,因为这个方程没有用到a。参数方程通过改变这点来作弊。在参数方程中,所有不同的方程共用一个变量,如下所示:

-\[ -\left \{ \begin{matrix} - f_a(t) = \cos(t) \\ - f_b(t) = \sin(t) -\end{matrix} \right. -\]

多个方程,但只有一个变量。如果我们改变了t的值,fa(t)fb(t)的输出都会发生变化。你可能会好奇这有什么用,答案其实很简单:对于参数曲线,如果我们用常用的标记来替代fa(t)fb(t),看起来就有些明朗了:

-\[ -\left \{ \begin{matrix} - x = \cos(t) \\ - y = \sin(t) -\end{matrix} \right. -\]

好了,通过一些神秘的t值将x/y坐标系联系起来。

-

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

- -

贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中x值和y值使用了不同的方程,与此不同的是,贝塞尔曲线的xy都用了“二项多项式”。那什么是二项多项式呢?

-

你可能记得高中所学的多项式,看起来像这样:

-\[ - f(x) = a \cdot x^3 + b \cdot x^2 + c \cdot x + d -\]

如果它的最高次项是就称为“三次”多项式,如果最高次项是,称为“二次”多项式,如果只含有x的项,它就是一条线(不过不含任何x的项它就不是一个多项式!)

-

贝塞尔曲线不是x的多项式,它是t的多项式,t的值被限制在0和1之间,并且含有ab等参数。它采用了二次项的形式,听起来很神奇但实际上就是混合不同值的简单描述:

-\[ -\begin{align*} - linear &= (1-t) + t \\ - square &= (1-t)^2 + 2 \cdot (1-t) \cdot t + t^2 \\ - cubic &= (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3 -\end{align*} -\]

我明白你在想什么:这看起来并不简单,但如果我们拿掉t并让系数乘以1,事情就会立马简单很多,看看这些二次项:

-\[ -\begin{align*} - linear &= \hskip{2.5em} 1 + 1 \\ - square &= \hskip{1.7em} 1 + 2 + 1\\ - cubic &= \hskip{0.85em} 1 + 3 + 3 + 1\\ - hypercubic &= 1 + 4 + 6 + 4 + 1 -\end{align*} -\]

需要注意的是,2与1+1相同,3相当于2+1或1+2,6相当于3+3...如你所见,每次我们增加一个维度,只要简单地将头尾置为1,中间的操作都是“将上面的两个数字相加”。现在就能很容易地记住了。

-

还有一个简单的办法可以弄清参数项怎么工作的:如果我们将(1-t)重命名为a,将t重命名为b,暂时把权重删掉,可以得到这个:

-\[ -\begin{align*} - linear &= BLUE[a] + RED[b] \\ - square &= BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot RED[b] + RED[b] \cdot RED[b] \\ - cubic &= BLUE[a] \cdot BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot BLUE[a] \cdot RED[b] + BLUE[a] \cdot RED[b] \cdot RED[b] + RED[b] \cdot RED[b] \cdot RED[b]\\ -\end{align*} -\]

基本上它就是“每个ab结合项”的和,在每个加号后面逐步的将a换成b。因此这也很简单。现在你已经知道了二次多项式,为了叙述的完整性,我将给出一般方程:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} -\]

这就是贝塞尔曲线完整的描述。在这个函数中的Σ表示了这是一系列的加法(用Σ下面的变量,从...=<值>开始,直到Σ上面的数字结束)。

- -
-

如何实现基本方程

-

我们可以用之前说过的方程,来简单地实现基本方程作为数学构造,如下:

-
function Bezier(n,t):
-  sum = 0
-  for(k=0; k<n; k++):
-    sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

我说我们“可以用”是因为我们不会这么去做:因为阶乘函数开销非常大。并且,正如我们在上面所看到的,我们不用阶乘也能够很容易地构造出帕斯卡三角形:一开始是[1],接着是[1,2,1],然后是[1,3,3,1]等等。下一行都比上一行多一个数,首尾都为1,中间的数字是上一行两边元素的和。

-

我们可以很快的生成这个列表,并在之后使用这个查找表而不用再计算二次多项式的系数:

-
lut = [      [1],           // n=0
-            [1,1],          // n=1
-           [1,2,1],         // n=2
-          [1,3,3,1],        // n=3
-         [1,4,6,4,1],       // n=4
-        [1,5,10,10,5,1],    // n=5
-       [1,6,15,20,15,6,1]]  // n=6
-
-binomial(n,k):
-  while(n >= lut.length):
-    s = lut.length
-    nextRow = new array(size=s+1)
-    nextRow[0] = 1
-    for(i=1, prev=s-1; i<prev; i++):
-      nextRow[i] = lut[prev][i-1] + lut[prev][i]
-    nextRow[s] = 1
-    lut.add(nextRow)
-  return lut[n][k]
-
-

这里做了些什么?首先,我们声明了一个足够大的查找表。然后,我们声明了一个函数来获取我们想要的值,并且确保当一个请求的n/k对不在LUT查找表中时,先将表扩大。我们的基本函数如下所示:

-
function Bezier(n,t):
-  sum = 0
-  for(k=0; k<=n; k++):
-    sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

完美。当然我们可以进一步优化。为了大部分的计算机图形学目的,我们不需要任意的曲线。我们需要二次曲线和三次曲线(实际上这篇文章没有涉及任意次的曲线,因此你会在其他地方看到与这些类似的代码),这说明我们可以彻底简化代码:

-
function Bezier(2,t):
-  t2 = t * t
-  mt = 1-t
-  mt2 = mt * mt
-  return mt2 + 2*mt*t + t2
-
-function Bezier(3,t):
-  t2 = t * t
-  t3 = t2 * t
-  mt = 1-t
-  mt2 = mt * mt
-  mt3 = mt2 * mt
-  return mt3 + 3*mt2*t + 3*mt*t2 + t3
-
-

现在我们知道如何代用码实现基本方程了。很好。

-
-

既然我们已经知道基本函数的样子,是时候添加一些魔法来使贝塞尔曲线变得特殊了:控制点。

-
; } - - }, - "control": { - "locale": "en-GB", - "title": "Controlling Bézier curvatures", - "getContent": function(handler) { return
- -

Bézier curves are (like all "splines") interpolation functions, meaning they take a set of points, and generate values somewhere "between" those points. (One of the consequences of this is that you'll never be able to generate a point that lies outside the outline for the control points, commonly called the "hull" for the curve. Useful information!). In fact, we can visualize how each point contributes to the value generated by the function, so we can see which points are important, where, in the curve.

-

The following graphs show the interpolation functions for quadratic and cubic curves, with "S" being the strength of a point's contribution to the total sum of the Bézier function. Click or click-drag to see the interpolation percentages for each curve-defining point at a specific t value.

- -
- - - -
-

Also shown is the interpolation function for a 15th order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.

-

If we want to change the curve, we need to change the weights of each point, effectively changing the interpolations. The way to do this is about as straight forward as possible: just multiply each point with a value that changes its strength. These values are conventionally called "Weights", and we can add them to our original Bézier function:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} - \cdot\ - \underset{weight}{\underbrace{w_i}} -\]

That looks complicated, but as it so happens, the "weights" are actually just the coordinate values we want our curve to have: for an nth - order curve, w0 is our start coordinate, wn is our last coordinate, and everything in between is a controlling coordinate. Say we want a cubic curve that starts at (120,160), is controlled by (35,200) and (220,260) and ends at (220,40), we use this Bézier curve:

-\[ -\left \{ \begin{matrix} - x = BLUE[120] \cdot (1-t)^3 + BLUE[35] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[220] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[220] \cdot t^3 \\ - y = BLUE[160] \cdot (1-t)^3 + BLUE[200] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[260] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[40] \cdot t^3 -\end{matrix} \right. -\]

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

- -

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.

- -
-

How to implement the weighted basis function

-

Given that we already know how to implement basis function, adding in the control points is remarkably easy:

-
function Bezier(n,t,w[]):
-  sum = 0
-  for(k=0; k<n; k++):
-    sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
-  return sum
-
-

And for the extremely optimized versions:

-
function Bezier(2,t,w[]):
-  t2 = t * t
-  mt = 1-t
-  mt2 = mt * mt
-  return w[0]*mt2 + w[1]*2*mt*t + w[2]*t2
-
-function Bezier(3,t,w[]):
-  t2 = t * t
-  t3 = t2 * t
-  mt = 1-t
-  mt2 = mt * mt
-  mt3 = mt2 * mt
-  return w[0]*mt3 + 3*w[1]*mt2*t + 3*w[2]*mt*t2 + w[3]*t3
-
-

And now we know how to program the weighted basis function.

-
-
; } - - }, - "extended": { - "locale": "en-GB", - "title": "The Bézier interval [0,1]", - "getContent": function(handler) { return
- -

Now that we know the mathematics behind Bézier curves, there's one curious thing that you may have noticed: they always run from t=0 to t=1. Why that particular interval?

-

It all has to do with how we run from "the start" of our curve to "the end" of our curve. If we have a value that is a mixture of two other values, then the general formula for this is:

-\[ - mixture = a \cdot value_1 + b \cdot value_2 -\]

The obvious start and end values here need to be a=1, b=0, so that the mixed value is 100% value 1, and 0% value 2, and a=0, b=1, so that the mixed value is 0% value 1 and 100% value 2. Additionally, we don't want "a" and "b" to be independent: if they are, then we could just pick whatever values we like, and end up with a mixed value that is, for example, 100% value 1 and 100% value 2. In principle that's fine, but for Bézier curves we always want mixed values between the start and end point, so we need to make sure we can never set "a" and "b" to some values that lead to a mix value that sums to more than 100%. And that's easy:

-\[ - m = a \cdot value_1 + (1 - a) \cdot value_2 -\]

With this we can guarantee that we never sum above 100%. By restricting a to values in the interval [0,1], we will always be somewhere between our two values (inclusively), and we will always sum to a 100% mix.

-

But... what if we use this form, used in the assumption that we will only ever use values between 0 and 1, and instead use values outside of that interval? Do things go horribly wrong? Well... not really, but we get to "see more".

-

In the case of Bézier curves, extending the interval simply makes our curve "keep going". Bézier curves are simply segments on some polynomial curve, so if we pick a wider interval we simply get to see more of the curve. So what do they look like?

-

The following two graphics show you Bézier curves rendered "the usual way", as well as the curves they "lie on" if we were to extend the t values much further. As you can see, there's a lot more "shape" hidden in the rest of the curve, and we can model those parts by moving the curve points around.

- - -

In fact, there are curves used in graphics design and computer modelling that do the opposite of Bézier curves, where rather than fixing the interval, and giving you free coordinates, they fix the coordinates, but give you freedom over the interval. A great example of this is the "Spiro" curve, which is a curve based on part of a Cornu Spiral, also known as Euler's Spiral. It's a very aesthetically pleasing curve and you'll find it in quite a few graphics packages like FontForge and Inkscape, having even been used in font design (such as for the Inconsolata font).

-
; } - - }, - "matrix": { - "locale": "en-GB", - "title": "Bézier curvatures as matrix operations", - "getContent": function(handler) { return
- -

We can also represent Bézier as matrix operations, by expressing the Bézier formula as a polynomial basis function, the weight matrix, and the actual coordinates as matrix. Let's look at what this means for the cubic curve:

-\[ - B(t) = P_1 \cdot (1-t)^3 + P_2 \cdot 3 \cdot (1-t)^2 \cdot t + P_3 \cdot 3 \cdot (1-t) \cdot t^2 + P_4 \cdot t^3 -\]

Disregarding our actual coordinates for a moment, we have:

-\[ - B(t) = (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3 -\]

We can write this as a sum of four expressions:

-\[ - \begin{matrix} - ... & = & (1-t)^3 \\ - & + & 3 \cdot (1-t)^2 \cdot t \\ - & + & 3 \cdot (1-t) \cdot t^2 \\ - & + & t^3 \\ - \end{matrix} -\]

And we can expand these expressions:

-\[ - \begin{matrix} - ... & = & (1-t) \cdot (1-t) \cdot (1-t) & = & -t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\ - & + & 3 \cdot (1-t) \cdot (1-t) \cdot t & = & 3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t \\ - & + & 3 \cdot (1-t) \cdot t \cdot t & = & -3 \cdot t^3 + 3 \cdot t^2 \\ - & + & t \cdot t \cdot t & = & t^3 \\ - \end{matrix} -\]

Furthermore, we can make all the 1 and 0 factors explicit:

-\[ - \begin{matrix} - ... & = & -1 \cdot t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\ - & + & +3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t + 0 \\ - & + & -3 \cdot t^3 + 3 \cdot t^2 + 0 \cdot t + 0 \\ - & + & +1 \cdot t^3 + 0 \cdot t^2 + 0 \cdot t + 0 \\ - \end{matrix} -\]

And that, we can view as a series of four matrix operations:

-\[ - \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-1 \\ 3 \\ -3 \\ 1\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}3 \\ -6 \\ 3 \\ 0\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-3 \\ 3 \\ 0 \\ 0\end{bmatrix} - + \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}1 \\ 0 \\ 0 \\ 0\end{bmatrix} -\]

If we compact this into a single matrix operation, we get:

-\[ - \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix} - -1 & 3 & -3 & 1 \\ - 3 & -6 & 3 & 0 \\ - -3 & 3 & 0 & 0 \\ - 1 & 0 & 0 & 0 - \end{bmatrix} -\]

This kind of polynomial basis representation is generally written with the bases in increasing order, which means we need to flip our t matrix horizontally, and our big "mixing" matrix upside down:

-\[ - \begin{bmatrix}1 & t & t^2 & t^3\end{bmatrix} \cdot \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} -\]

And then finally, we can add in our original coordinates as a single third matrix:

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

We can perform the same trick for the quadratic curve, in which case we end up with:

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

If we plug in a t value, and then multiply the matrices, we will get exactly the same values as when we evaluate the original polynomial function, or as when we evaluate the curve using progessive linear interpolation.

-

-So: why would we bother with matrices? Matrix representations allow us to discover things about functions that would otherwise be hard to tell. It turns out that the curves form triangular matrices, and they have a determinant equal to the product of the actual coordinates we use for our curve. It's also invertible, which means there's a ton of properties that are all satisfied. Of course, the main question is: "Why is this useful to us, now?", and the answer to that is that it's not immediately useful, but you'll be seeing some instances where certain curve properties can be either computed via function manipulation, or via clever use of matrices, and sometimes the matrix approach can be (drastically) faster.

-

So for now, just remember that we can represent curves this way, and let's move on.

-
; } - - }, - "decasteljau": { - "locale": "en-GB", - "title": "de Casteljau's algorithm", - "getContent": function(handler) { return
- -

If we want to draw Bézier curves we can run through all values of t from 0 to 1 and then compute the weighted basis function, getting the x/y values we need to plot, but the more complex the curve gets, the more expensive this becomes. Instead, we can use "de Casteljau's algorithm" to draw curves, which is a geometric approach to drawing curves, and really easy to implement. So easy, in fact, you can do it by hand with a pencil and ruler.

-

Rather than using our calculus function to find x/y values for t, let's do this instead:

-
    -
  • treat t as a ratio (which it is). t=0 is 0% along a line, t=1 is 100% along a line.
  • -
  • Take all lines between the curve's defining points. For an order n curve, that's n lines.
  • -
  • Place markers along each of these line, at distance t. So if t is 0.2, place the mark at 20% from the start, 80% from the end.
  • -
  • Now form lines between those points. This gives n-1 lines.
  • -
  • Place markers along each of these line at distance t.
  • -
  • Form lines between those points. This'll be n-2 lines.
  • -
  • place markers, form lines, place markers, etc.
  • -
  • repeat this until you have only one line left. The point t on that line coincides with the original curve point at t.
  • -
- -
-

How to implement de Casteljau's algorithm

-

Let's just use the algorithm we just specified, and implement that:

-
function drawCurve(points[], t):
-  if(points.length==1):
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      newpoints[i] = (1-t) * points[i] + t * points[i+1]
-    drawCurve(newpoints, t)
-
-

And done, that's the algorithm implemented. Except usually you don't get the luxury of overloading the "+" operator, so let's also give the code for when you need to work with x and y values:

-
function drawCurve(points[], t):
-  if(points.length==1):
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      x = (1-t) * points[i].x + t * points[i+1].x
-      y = (1-t) * points[i].y + t * points[i+1].y
-      newpoints[i] = new point(x,y)
-    drawCurve(newpoints, t)
-
-

So what does this do? This draws a point, if the passed list of points is only 1 point long. Otherwise it will create a new list of points that sit at the t ratios (i.e. the "markers" outlined in the above algorithm), and then call the draw function for this new list.

-
-

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.

- -
; } - - }, - "flattening": { - "locale": "en-GB", - "title": "Simplified drawing", - "getContent": function(handler) { return
- -

We can also simplify the drawing process by "sampling" the curve at certain points, and then joining those points up with straight lines, a process known as "flattening", as we are reducing a curve to a simple sequence of straight, "flat" lines.

-

We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.

- - -

Try clicking on the sketch and using your up and down arrow keys to lower the number of segments for both the quadratic and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.

- -
-

How to implement curve flattening

-

Let's just use the algorithm we just specified, and implement that:

-
function flattenCurve(curve, segmentCount):
-  step = 1/segmentCount;
-  coordinates = [curve.getXValue(0), curve.getYValue(0)]
-  for(i=1; i <= segmentCount; i++):
-    t = i*step;
-    coordinates.push[curve.getXValue(t), curve.getYValue(t)]
-  return coordinates;
-
-

And done, that's the algorithm implemented. That just leaves drawing the resulting "curve" as a sequence of lines:

-
function drawFlattenedCurve(curve, segmentCount):
-  coordinates = flattenCurve(curve, segmentCount)
-  coord = coordinates[0], _coords;
-  for(i=1; i < coordinates.length; i++):
-    _coords = coordinates[i]
-    line(coords, _coords)
-    coords = _coords
-
-

We start with the first coordinate as reference point, and then just draw lines between each point and its next point.

-
-
; } - - }, - "splitting": { - "locale": "en-GB", - "title": "Splitting curves", - "getContent": function(handler) { return
- -

With de Casteljau's algorithm we 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.

- -
-

implementing curve splitting

-

We can implement curve splitting by bolting some extra logging onto the de Casteljau function:

-
left=[]
-right=[]
-function drawCurve(points[], t):
-  if(points.length==1):
-    left.add(points[0])
-    right.add(points[0])
-    draw(points[0])
-  else:
-    newpoints=array(points.size-1)
-    for(i=0; i<newpoints.length; i++):
-      if(i==0):
-        left.add(points[i])
-      if(i==newpoints.length-1):
-        right.add(points[i+1])
-      newpoints[i] = (1-t) * points[i] + t * points[i+1]
-    drawCurve(newpoints, t)
-
-

After running this function for some value t, the left and right arrays will contain all the coordinates for two new curves - one to the "left" of our t value, the other on the "right", of the same order as the original curve, and overlayed exactly on the original curve.

-
-

This is best illustrated with an animated graphic (click to play/pause):

- -
; } - - }, - "matrixsplit": { - "locale": "en-GB", - "title": "Splitting curves using matrices", - "getContent": function(handler) { return
- -

Another way to split curves is to exploit the matrix representation of a Bézier curve. In the section on matrices we saw that we can represent curves as matrix multiplications. Specifically, we saw these two forms for the quadratic, and cubic curves, respectively (using the reversed Bézier coefficients vector for legibility):

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

and

-\[ - B(t) = \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0\\ - -3 & 3 & 0 & 0\\ - 3 & -6 & 3 & 0\\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

Let's say we want to split the curve at some point t = z, forming two new (obviously smaller) Bézier curves. To find the coordinates for these two Bézier curves, we can use the matrix representation and some linear algebra. First, we split out the the actual "point on the curve" information as a new matrix multiplication:

-\[ - B(t) = - \begin{bmatrix} - 1 & (z \cdot t) & (z \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

and

-\[ - B(t) = - \begin{bmatrix} - 1 & (z \cdot t) & (z \cdot t)^2 & (z \cdot t)^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 & t^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0\\ - 0 & z & 0 & 0\\ - 0 & 0 & z^2 & 0\\ - 0 & 0 & 0 & z^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -3 & 3 & 0 & 0 \\ - 3 & -6 & 3 & 0 \\ - -1 & 3 & -3 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} -\]

If we could compact these matrices back to a form [t values] · [bezier matrix] · [column matrix], with the first two staying the same, then that column matrix on the right would be the coordinates of a new Bézier curve that describes the first segment, from t = 0 to t = z. As it turns out, we can do this quite easily, by exploiting some simple rules of linear algebra (and if you don't care about the derivations, just skip to the end of the box for the results!).

- -
-

Deriving new hull coordinates

-

Deriving the two segments upon splitting a curve takes a few steps, and the higher the curve order, the more work it is, so let's look at the quadratic curve first:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \underset{we\ turn\ this...}{\underbrace{\kern 2.25em Z \cdot M \kern 2.25em}} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \underset{...into\ this...}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \underset{...to\ get\ this!}{\underbrace{ \kern 1.25em \cdot \kern 1.25em Q \kern 1.25em \cdot \kern 1.25em}} - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

We do this, because [M · M-1 -] is the identity matrix (a bit like multiplying something by x/x in calculus. It doesn't do anything to the function, but it does allow you to rewrite it to something that may be easier to work with, or can be broken up differently). Adding that as matrix multiplication has no effect on the total formula, but it does allow us to change the matrix sequence [something · M] to a sequence [M · something], and that makes a world of difference: if we know what [M-1 · Z · M] is, we can apply that to our coordinates, and be left with a proper matrix representation of a quadratic Bézier curve (which is [T · M · P]), with a new set of coordinates that represent the curve from t = 0 to t = z. So let's get computing:

-\[ - Q = M^{-1} \cdot Z \cdot M = - \begin{bmatrix} - 1 & 0 & 0 \\ - 1 & \frac{1}{2} & 0 \\ - 1 & 1 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - = - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} -\]

Excellent! Now we can form our new quadratic curve:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot M \cdot Q \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \cdot - \left ( - Q - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \left ( - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1 - \end{bmatrix} -\]

- -Brilliant -: if we want a subcurve from t = 0 to t = z, we can keep the first coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the start point, and the new end point is a mixture that looks oddly similar to a bernstein polynomial of degree two, except it uses (z-1) rather than (1-z)... These new coordinates are actually really easy to compute directly!

-

Of course, that's only one of the two curves. Getting the section from t = z to t = 1 requires doing this again. We first observe what what we just did is actually evaluate the general interval [0,z], which we wrote down simplified becuase of that zero, but we actually evaluated this:

-\[ - B(t) = - \begin{bmatrix} - 1 & ( 0 + z \cdot t) & ( 0 + z \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - 0 & z & 0 \\ - 0 & 0 & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

If we want the interval [z,1], we will be evaluating this instead:

-\[ - B(t) = - \begin{bmatrix} - 1 & ( z + (1-z) \cdot t) & ( z + (1-z) \cdot t)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & z & z^2 \\ - 0 & 1-z & 2 \cdot z \cdot (1-z) \\ - 0 & 0 & (1-z)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} -\]

We're going to do the same trick, to turn [something · M] into [M · something]:

-\[ - Q' = M^{-1} \cdot Z' \cdot M = - \begin{bmatrix} - 1 & 0 & 0 \\ - 1 & \frac{1}{2} & 0 \\ - 1 & 1 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & z & z^2 \\ - 0 & 1-z & 2 \cdot z \cdot (1-z) \\ - 0 & 0 & (1-z)^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - = - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} -\]

So, our final second curve looks like:

-\[ - B(t) = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot M \cdot Q \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - M - \cdot - \left ( - Q' - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \left ( - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - \right ) -\]\[ - = - \begin{bmatrix} - 1 & t & t^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - 1 & 0 & 0 \\ - -2 & 2 & 0 \\ - 1 & -2 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z \cdot P_3 - (z-1) \cdot P_2 \\ - P_3 - \end{bmatrix} -\]

- -Nice -: we see the same as before; can keep the last coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the end point, and the new start point is a mixture that looks oddly similar to a bernstein polynomial of degree two, except it uses (z-1) rather than (1-z). These new coordinates are also really easy to compute directly!

-
-

So, using linear algebra rather than de Casteljau's algorithm, we have determined that for any quadratic curve split at some value t = z, we get two subcurves that are described as Bézier curves with simple-to-derive coordinates.

-\[ - \begin{bmatrix} - 1 & 0 & 0 \\ - -(z-1) & z & 0 \\ - (z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1 - \end{bmatrix} -\]

and

-\[ - \begin{bmatrix} - (z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\ - 0 & -(z-1) & z \\ - 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 - \end{bmatrix} - = - \begin{bmatrix} - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z \cdot P_3 - (z-1) \cdot P_2 \\ - P_3 - \end{bmatrix} -\]

We can do the same for cubic curves. However, I'll spare you the actual derivation (don't let that stop you from writing that out yourself, though) and simply show you the resulting new coordinate sets:

-\[ - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - -(z-1) & z & 0 & 0 \\ - (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 & 0 \\ - -(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - P_1 \\ - z \cdot P_2 - (z-1) \cdot P_1 \\ - z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\ - z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 - \end{bmatrix} -\]

and

-\[ - \begin{bmatrix} - -(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1)^3 \cdot z^2 & z^3 \\ - 0 & (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 \\ - 0 & 0 & -(z-1) & z \\ - 0 & 0 & 0 & 1 - \end{bmatrix} - \cdot - \begin{bmatrix} - P_1 \\ P_2 \\ P_3 \\ P_4 - \end{bmatrix} - = - \begin{bmatrix} - z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \\ - z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \\ - z \cdot P_4 - (z-1) \cdot P_3 \\ - P_4 - \end{bmatrix} -\]

So, looking at our matrices, did we really need to compute the second segment matrix? No, we didn't. Actually having one segment's matrix means we implicitly have the other: push the values of each row in the matrix -Q - to the right, with zeroes getting pushed off the right edge and appearing back on the left, and then flip the matrix vertically. Presto, you just "calculated" -Q' -.

-

Implementing curve splitting this way requires less recursion, and is just straight arithmetic with cached values, so can be cheaper on systems were recursion is expensive. If you're doing computation with devices that are good at matrix multiplication, chopping up a Bézier curve with this method will be a lot faster than applying de Casteljau.

-
; } - - }, - "reordering": { - "locale": "en-GB", - "title": "Lowering and elevating curve order", - "getContent": function(handler) { return
- -

One interesting property of Bézier curves is that an nth - order curve can always be perfectly represented by an (n+1)th - order curve, by giving the higher order curve specific control points.

-

If we have a curve with three points, then we can create a four point curve that exactly reproduce the original curve as long as we give it the same start and end points, and for its two control points we pick "1/3rd start + 2/3rd control" and "2/3rd control + 1/3rd end", and now we have exactly the same curve as before, except represented as a cubic curve, rather than a quadratic curve.

-

The general rule for raising an nth - order curve to an (n+1)th - order curve is as follows (observing that the start and end weights are the same as the start and end weights for the old curve):

-\[ - Bézier(k,t) = \sum_{i=0}^{k} - \underset{binomial\ term}{\underbrace{\binom{k}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}} - \ \cdot \ - \underset{new\ weights}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}} - \ ,\ with\ k = n+1\ and\ w_{i-1}=0\ when\ i = 0 -\]

However, this rule also has as direct consequence that you cannot generally safely lower a curve from nth - order to (n-1)th - order, because the control points cannot be "pulled apart" cleanly. We can try to, but the resulting curve will not be identical to the original, and may in fact look completely different.

-

We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch and press your up and down arrow keys to elevate or lower the curve order.

- -

There is a good, if mathematical, explanation on the matrices necessary for optimal reduction over on Sirver's Castle, which given time will find its way in a more direct description into this article.

-
; } - - }, - "derivatives": { - "locale": "en-GB", - "title": "Derivatives", - "getContent": function(handler) { return
- -

There's a number of useful things that you can do with Bézier curves based on their derivative, and one of the more amusing observations about Bézier curves is that their derivatives are, in fact, also Bézier curves. In fact, the derivation of a Bézier curve is relatively straight forward, although we do need a bit of math. First, let's look at the derivative rule for Bézier curves, which is:

-\[ - Bézier'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot Bézier(n-1,t)_i -\]

which we can also write (observing that b in this formula is the same as our w weights, and that n times a summation is the same as a summation where each term is multiplied by n) as:

-\[ - Bézier'(n,t) = \sum_{i=0}^{n-1} Bézier(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i) -\]

Or, in plain text: the derivative of an nth degree Bézier curve is an (n-1)th degree Bézier curve, with one fewer term, and new weights w'0...w'n-1 derived from the original weights as n(wi+1 - wi), so for a 3rd degree curve, with four weights, the derivative has three new weights w'0 = 3(w1-w0), w'1 = 3(w2-w1) and w'2 = 3(w3-w2).

- -
-

"Slow down, why is that true?"

-

Sometimes just being told "this is the derivative" is nice, but you might want to see why this is indeed the case. As such, let's have a look at the proof for this derivative. First off, the weights are independent of the full Bézier function, so the derivative involves only the derivative of the polynomial basis function. So, let's find that:

-\[ - B_{n,k}(t) \frac{d}{dt} = {n \choose k} t^k (1-t)^{n-k} \frac{d}{dt} -\]

Applying the product and chain rules gives us:

-\[\begin{array}{l} - ... &= {n \choose k} \left ( - k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1 - \right ) -\end{array}\]

Which is hard to work with, so let's expand that properly:

-\[\begin{array}{l} - ... &= \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} -\end{array}\]

Now, the trick is to turn this expression into something that has binomial coefficients again, so we want to end up with things that look like "x! over y!(x-y)!". If we can do that in a way that involves terms of n-1 and k-1, we'll be on the right track.

-\[\begin{array}{l} - ... &= \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\ - - ... &= n \left ( - \frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k} - \right ) \\ - - ... &= n \left ( - \frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{(k-1)} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k} - \right ) -\end{array}\]

And that's the first part done: the two components inside the parentheses are actually regular, lower order Bezier expressions:

-\[\begin{array}{l} - ... &= n \left ( - \frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k} - \right ) - \ ,\ with\ x=n-1,\ y=k-1 - \\ - - - ... &= n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right ) -\end{array}\]

Now to apply this to our weighted Bezier curves. We'll write out the plain curve formula that we saw earlier, and then work our way through to its derivative:

-\[\begin{array}{l} - Bézier_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + ... \\ - Bézier_{n,k}(t) \frac{d}{dt} &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + \\ - & & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + \\ - & & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + \\ - & & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + \\ - & & ... -\end{array}\]

If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for k we see the following:

-\[\begin{array}{l} - n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\ - n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\ - n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\ - n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\ - ... &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\ - ... & & & -\end{array}\]

Two of these terms fall way: the first term falls away because there is no -1st term in a summation. As such, it always contributes "nothing", so we can safely completely ignore it for the purpose of finding the derivative function. The other term is the very last term in this expansion: one involving Bn-1,n -. This term would have a binomial coefficient of [i choose i+1], which is a non-existent binomial coefficient. Again, this term would contribute "nothing", so we can ignore it, too. This means we're left with:

-\[\begin{array}{l} - n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\ - n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& \ n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\ - n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\ - ... -\end{array}\]

And that's just a summation of lower order curves:

-\[ - Bézier_{n,k}(t) \frac{d}{dt} = n \cdot B_{(n-1),BLUE[0]}(t) \cdot (w_1 - w_0) - + n \cdot B_{(n-1),RED[1]}(t) \cdot (w_2 - w_1) - + n \cdot B_{(n-1),MAGENTA[2]}(t) \cdot (w_3 - w_2) - \ + \ ... -\]

We can rewrite this as a normal summation, and we're done:

-\[ - Bézier_{n,k}(t) \frac{d}{dt} = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k) - = \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underset{derivative\ weights} - {\underbrace{n \cdot (w_{k+1} - w_k)}} -\]
-

Let's rewrite that in a form similar to our original formula, so we can see the difference. We will first list our original formula for Bézier curves, and then the derivative:

-\[ - Bézier(n,t) = \sum_{i=0}^{n} - \underset{binomial\ term}{\underbrace{\binom{n}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}} - \cdot\ - \underset{weight}{\underbrace{w_i}} -\]\[ - Bézier'(n,t) = \sum_{i=0}^{k} - \underset{binomial\ term}{\underbrace{\binom{k}{i}}} - \cdot\ - \underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}} - \cdot\ - \underset{derivative\ weight}{\underbrace{n \cdot (w_{i+1} - w_i)}} - {\ , \ with \ k=n-1} -\]

What are the differences? In terms of the actual Bézier curve, virtually nothing! We lowered the order (rather than n, it's now n-1), but it's still the same Bézier function. The only real difference is in how the weights change when we derive the curve's function. If we have four points A, B, C, and D, then the derivative will have three points, the second derivative two, and the third derivative one:

-\[ \begin{array}{l} - B(n,t), & & w = \{A,B,C,D\} \\ - B'(n,t), & n = 3, & w' = \{A',B',C'\} &= \{3 \cdot (B-A), {\ } 3 \cdot (C-B), {\ } 3 \cdot (D-C)\} \\ - B''(n,t), & n = 2, & w'' = \{A'',B''\} &= \{2 \cdot (B'-A'), {\ } 2 \cdot (C'-B')\} \\ - B'''(n,t), & n = 1, & w''' = \{A'''\} &= \{1 \cdot (B''-A'')\} -\end{array} \]

We can keep performing this trick for as long as we have more than one weight. Once we have one weight left, the next step will see k = 0, and the result of our "Bézier function" summation is zero, because we're not adding anything at all. As such, a quadratic curve has no second derivative, a cubic curve has no third derivative, and generalized: an nth - order curve has n-1 (meaningful) derivatives, with any further derivative being zero.

-
; } - - }, - "pointvectors": { - "locale": "en-GB", - "title": "Tangents and normals", - "getContent": function(handler) { return
- -

If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested in are the tangent vector and normal vector for curve points. These are actually really easy to find. For moving, and orienting, along a curve we use the tangent, which indicates the direction travel at specific points, and is literally just the first derivative of our curve:

-\[ -\left \{ \begin{matrix} - tangent_x(t) = B'_x(t) \\ - tangent_y(t) = B'_y(t) -\end{matrix} \right. -\]

This gives us the directional vector we want. We can normalize it to give us uniform directional vectors (having a length of 1.0) at each point, and then do whatever it is we want to do based on those directions:

-\[ - d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2} -\]\[ -\left \{ \begin{matrix} - \hat{x}(t) = || tangent_x(t) || - =\frac{tangent_x(t)}{ || tangent(t) || } - = \frac{B'_x(t)}{d} \\ - \hat{y}(t) = || tangent_y(t) || - = \frac{tangent_y(t)}{ || tangent(t) || } - = \frac{B'_y(t)}{d} -\end{matrix} \right. -\]

The tangent is very useful for moving along a line, but what if we want to move away from the curve instead, perpendicular to the curve at some point t? In that case we want the "normal" vector. This vector runs at a right angle to the direction of the curve, and is typically of length 1.0, so all we have to do is rotate the normalized directional vector and we're done:

-\[ -\left \{ \begin{array}{l} - normal_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\ - normal_y(t) = \underset{quarter\ circle\ rotation} {\underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }} = \hat{x}(t) -\end{array} \right. -\] -
-

Rotating coordinates is actually very easy, if you know the rule for it. You might find it explained as "applying a rotation matrix, which is what we'll look at here, too. Essentially, the idea is to take the circles over which we can rotate, and simply "sliding the coordinates" over these circles by the desired -angle. If we want a quarter circle turn, we take the coordinate, slide it along the cirle by a quarter turn, and done.

-

To turn any point (x,y) into a rotated point (x',y') (over 0,0) by some angle φ, we apply this nicely easy computation:

-\[\begin{array}{l} - x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\ - y' = x \cdot \sin(\phi) + y \cdot \cos(\phi) -\end{array}\]

Which is the "long" version of the following matrix transformation:

-\[ - \begin{bmatrix} - x' \\ y' - \end{bmatrix} - = - \begin{bmatrix} - \cos(\phi) & -\sin(\phi) \\ - \sin(\phi) & \cos(\phi) - \end{bmatrix} - \begin{bmatrix} - x \\ y - \end{bmatrix} -\]

And that's all we need to rotate any coordinate. Note that for quarter, half and three quarter turns these functions become even easier, since sin and cos for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.

-

But -why - does this work? Why this matrix multiplication? Wikipedia (Technically, Thomas Herter and Klaus Lott) tells us that a rotation matrix can be -treated as a sequence of three (elementary) shear operations. When we combine this into a single matrix operation (because all matrix multiplications can be collapsed), we get the matrix that you see above. DataGenetics have an excellent article about this very thing: it's really quite cool, and I strongly recommend taking a quick break from this primer to read that article.

-
-

The following two graphics show the tangent and normal along a quadratic and cubic curve, with the direction vector coloured blue, and the normal vector coloured red (the markers are spaced out evenly as t-intervals, not spaced equidistant).

- -
- - -
-
; } - - }, - "components": { - "locale": "en-GB", - "title": "Component functions", - "getContent": function(handler) { return
- -

One of the first things people run into when they start using Bézier curves in their own programs is "I know how to draw the curve, but how do I determine the bounding box?". It's actually reasonably straight forward to do so, but it requires having some knowledge on exploiting math to get the values we need. For bounding boxes, we aren't actually interested in the curve itself, but only in its "extremities": the minimum and maximum values the curve has for its x- and y-axis values. If you remember your calculus (provided you ever took calculus, otherwise it's going to be hard to remember) we can determine function extremities using the first derivative of that function, but this poses a problem, since our function is parametric: every axis has its own function.

-

The solution: compute the derivative for each axis separately, and then fit them back together in the same way we do for the original.

-

Let's look at how a parametric Bézier curve "splits up" into two normal functions, one for the x-axis and one for the y-axis. Note the left-most figure is again an interactive curve, without labeled axes (you get coordinates in the graph instead). The center and right-most figures are the component functions for computing the x-axis value, given a value for t (between 0 and 1 inclusive), and the y-axis value, respectively.

-

If you move points in a curve sideways, you should only see the middle graph change; likely, moving points vertically should only show a change in the right graph.

- - -
; } - - }, - "extremities": { - "locale": "en-GB", - "title": "Finding extremities: root finding", - "getContent": function(handler) { return
- -

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

-

Quadratic curves: linear derivatives.

-

Finding the solution for "where is this line 0" should be trivial:

-\[ -\begin{align} - l(x) = ax + b &= 0,\\ - ax + b &= 0,\\ - ax &= -b \\ - x &= \frac{-b}{a} -\end{align} -\]

Done. And quadratic curves have no meaningful second derivative, so we're really done.

-

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

-

First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the derivatives section:

-\[ -\begin{align} - B(t)\ uses\ \{ p_1,p_2,p_3,p_4 \} \\ - B'(t)\ uses\ \{ v_1.v_2,v_3 \},\ where\ v_1 = 3(p_2-p_1),\ v_2 = 3(p_3-p_2),\ v_3 = 3(p_4-p_3) -\end{align} -\]

And then, using these v values, we can find out what our a, b, and c should be:

-\[ -\begin{align} - B'(t) &= v_1(1-t)^2 + 2v_2(1-t)t + v_3t^2 \\ - ... &= v_1(t^2 - 2t + 1) + 2v_2(t-t^2) + v_3t^2 \\ - ... &= v_1t^2 - 2v_1t + v_1 + 2v_2t - 2v_2t^2 + v_3t^2 \\ - ... &= v_1t^2 - 2v_2t^2 + v_3t^2 - 2v_1t + v_1 + 2v_2t \\ - ... &= (v_1-2v_2+v_3)t^2 + 2(v_2-v_1)t + v_1 -\end{align} -\]

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:

-\[ -\begin{align} - a &= v_1-2v_2+v_3 = 3(-p_1 + 3p_2 - 3p_3 + p_4) \\ - b &= 2(v_2-v_1) = 6(p_1 - 2p_2 + p_3) \\ - c &= v_1 = 3(p_2-p_1) -\end{align} -\]

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.

-

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

-\[ - very\ hard:\ solve\ at^3 + bt^2 + ct + d = 0\\ - easier:\ solve\ t^3 + pt + q = 0 -\]

This is easier because for the "easier formula" we can 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).

-

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

- -
-

Implementing Cardano's algorithm for finding all real roots

-

The "real roots" part is fairly important, because while you cannot take a square, cube, etc. root of a negative number in the "real" number space (denoted with ℝ), this is perfectly fine in the "complex" number space (denoted with ℂ). And, as it so happens, Cardano is also attributed as the first mathematician in history to have made use of complex numbers in his calculations. For this very algorithm!

-
// A helper function to filter for values in the [0,1] interval:
-function accept(t) {
-  return 0<=t && t <=1;
-}
-
-// A real-cuberoots-only function:
-function crt(v) {
-  if(v<0) return -Math.pow(-v,1/3);
-  return Math.pow(v,1/3);
-}
-
-// Now then: given cubic coordinates {pa, pb, pc, pd} find all roots.
-function getCubicRoots(pa, pb, pc, pd) {
-  var d = (-pa + 3*pb - 3*pc + pd),
-  a = (3*pa - 6*pb + 3*pc) / d,
-  b = (-3*pa + 3*pb) / d,
-  c = pa / d;
-
-  var p = (3*b - a*a)/3,
-  p3 = p/3,
-  q = (2*a*a*a - 9*a*b + 27*c)/27,
-  q2 = q/2,
-  discriminant = q2*q2 + p3*p3*p3;
-
-  // and some variables we're going to use later on:
-  var u1,v1,root1,root2,root3;
-
-  // three possible real roots:
-  if (discriminant < 0) {
-    var mp3  = -p/3,
-    mp33 = mp3*mp3*mp3,
-    r    = sqrt( mp33 ),
-    t    = -q / (2*r),
-    cosphi = t<-1 ? -1 : t>1 ? 1 : t,
-    phi  = acos(cosphi),
-    crtr = cuberoot(r),
-    t1   = 2*crtr;
-    root1 = t1 * cos(phi/3) - a/3;
-    root2 = t1 * cos((phi+2*pi)/3) - a/3;
-    root3 = t1 * cos((phi+4*pi)/3) - a/3;
-    return [root1, root2, root3].filter(accept);
-  }
-
-  // three real roots, but two of them are equal:
-  if(discriminant === 0) {
-    u1 = q2 < 0 ? cuberoot(-q2) : -cuberoot(q2);
-    root1 = 2*u1 - a/3;
-    root2 = -u1 - a/3;
-    return [root1, root2].filter(accept);
-  }
-
-  // one real root, two complex roots
-  var sd = sqrt(discriminant);
-  u1 = cuberoot(sd - q2);
-  v1 = cuberoot(sd + q2);
-  root1 = u1 - v1 - a/3;
-  return [root1].filter(accept);
-}
-
-
-

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.

-

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.

-

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 root finding method (yes, after -that - Newton), which we can make use of.

-

The Newton-Raphson approach consists of picking a value t (any will do), and getting the corresponding value 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:

-\[ - t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_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)

-

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?

-

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.

-

In conclusion:

-

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:

- - -
; } - - }, - "boundingbox": { - "locale": "en-GB", - "title": "Bounding boxes", - "getContent": function(handler) { return
- -

If we have the extremities, and the start/end points, a simple for loop that tests for min/max values for x and y means we have the four values we need to box in our curve:

-

-Computing the bounding box for a Bézier curve:

-
    -
  1. Find all t value(s) for the curve derivative's x- and y-roots.
  2. -
  3. Discard any t value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].
  4. -
  5. Determine the lowest and highest value when plugging the values t=0, t=1 and each of the found roots into the original functions: the lowest value is the lower bound, and the highest value is the upper bound for the bounding box we want to construct.
  6. -
-

Applying this approach to our previous root finding, we get the following bounding boxes (with all curve extremity points shown on the curve):

- - -

We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first need to look at how aligning works.

-
; } - - }, - "aligning": { - "locale": "en-GB", - "title": "Aligning curves", - "getContent": function(handler) { return
- -

While there are an incredible number of curves we can define by varying the x- and y-coordinates for the control points, not all curves are actually distinct. For instance, if we define a curve, and then rotate it 90 degrees, it's still the same curve, and we'll find its extremities in the same spots, just at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to "axis-align" it.

-

Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first point lies on (0,0), which turns our n term polynomial functions into n-1 term functions. The order stays the same, but we have less terms. Then, we can rotate the curves so that the last point always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the function for the y-component to an n-2 term function. For instance, if we have a cubic curve such as this:

-\[ -\left \{ \begin{matrix} - x = BLUE[120] \cdot (1-t)^3 BLUE[+ 35] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 220] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 220] \cdot t^3 \\ - y = BLUE[160] \cdot (1-t)^3 BLUE[+ 200] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 260] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 40] \cdot t^3 -\end{matrix} \right. -\]

Then translating it so that the first coordinate lies on (0,0), moving all x coordinates by -120, and all y coordinates by -160, gives us:

-\[ -\left \{ \begin{matrix} - x = BLUE[0] \cdot (1-t)^3 BLUE[- 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 100] \cdot t^3 \\ - y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 120] \cdot t^3 -\end{matrix} \right. -\]

If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded for illustrative purposes here) become:

-\[ -\left \{ \begin{matrix} - x = BLUE[0] \cdot (1-t)^3 BLUE[+ 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 12] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\ - y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 140] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 0] \cdot t^3 -\end{matrix} \right. -\]

If we drop all the zero-terms, this gives us:

-\[ -\left \{ \begin{array}{l} - x = BLUE[85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 13] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\ - y = BLUE[40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 141] \cdot 3 \cdot (1-t) \cdot t^2 -\end{array} \right. -\]

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:

- - -
; } - - }, - "tightbounds": { - "locale": "en-GB", - "title": "Tight boxes", - "getContent": function(handler) { return
- -

With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for curves. We first align our curve, recording the translation we performed, "T", and the rotation angle we used, "R". We then determine the aligned curve's normal bounding box. Once we have that, we can map that bounding box back to our original curve by rotating it by -R, and then translating it by -T. We now have nice tight bounding boxes for our curves:

- - -

These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding precision is often not worth it. If there is high demand for it, I'll add a section on how to precisely compute the best fit bounding box, but the maths is fairly gruelling and just not really worth spending time on.

-
; } - - }, - "inflections": { - "locale": "en-GB", - "title": "Curve inflections", - "getContent": function(handler) { return
- -

Now that we know how to align a curve, there's one more thing we can calculate: inflection points. Imagine we have a variable size circle that we can slide up against our curve. We place it against the curve and adjust its radius so that where it touches the curve, the curvatures of the curve and the circle are the same, and then we start to slide the circle along the curve - for quadratic curves, we can always do this without the circle behaving oddly: we might have to change the radius of the circle as we slide it along, but it'll always sit against the same side of the curve.

-

But what happens with cubic curves? Imagine we have an S curve and we place our circle at the start of the curve, and start sliding it along. For a while we can simply adjust the radius and things will be fine, but once we get to the midpoint of that S, something odd happens: the circle "flips" from one side of the curve to the other side, in order for the curvatures to keep matching. This is called an inflection, and we can find out where those happen relatively easily.

-

What we need to do is solve a simple equation:

-\[ - C(t) = 0 -\]

What we're saying here is that given the curvature function C(t), we want to know for which values of t this function is zero, meaning there is no "curvature", which will be exactly at the point between our circle being on one side of the curve, and our circle being on the other side of the curve. So what does C(t) look like? Actually something that seems not too hard:

-\[ - C(t) = Bézier_x\prime(t) \cdot Bézier_y{\prime\prime}(t) - Bézier_y\prime(t) \cdot Bézier_x{\prime\prime}(t) -\]

So the function C(t) is wholly defined by the first and second derivative functions for the parametric dimensions of our curve. And as already shown, derivatives of Bézier curves are just simpler Bézier curves, with very easy to compute new coefficients, so this should be pretty easy.

-

However as we've seen in the section on aligning, aligning lets us simplify things a lot, by completely removing the contributions of the first coordinate from most mathematical evaluations, and removing the last y coordinate as well by virtue of the last point lying on the x-axis. So, while we can evaluate C(t) = 0 for our curve, it'll be much easier to first axis-align the curve and then evalutating the curvature function.

- -
-

Let's derive the full formula anyway

-

Of course, before we do our aligned check, let's see what happens if we compute the curvature function without axis-aligning. We start with the first and second derivatives, given our basis functions:

-\[ -\begin{align*} - & Bézier(t) = x_1(1-t)^3 + 3x_2(1-t)^2t + 3x_3(1-t)t^2 + x_4t^3 \\ - & Bézier^\prime(t) = a(1-t)^2 + 2b(1-t)^t + ct^2\ \left\{ a=3(x_2-x_1),b=3(x_3-x_2),c=3(x_4-x_3) \right\} \\ - & Bézier^{\prime\prime}(t) = u(1-t) + vt\ \left\{ u=2(b-a),v=2(c-b) \right\}\ -\end{align*} -\]

And of course the same functions for y:

-\[ -\begin{align*} - & Bézier(t) = y_1(1-t)^3 + 3y_2(1-t)^2t + 3y_3(1-t)t^2 + y_4t^3 \\ - & Bézier^\prime(t) = d(1-t)^2 + 2e(1-t)^t + ft^2\\ - & Bézier^{\prime\prime}(t) = w(1-t) + zt -\end{align*} -\]

Asking a computer to now compose the C(t) function for us (and to expand it to a readable form of simple terms) gives us this rather overly complicated set of arithmetic expressions:

-\[ -\begin{array} - -18 t^2 x_2 y_1+36 t^2 x_3 y_1-18 t^2 x_4 y_1+18 t^2 x_1 y_2-54 t^2 x_3 y_2 \\ - +36 t^2 x_4 y_2-36 t^2 x_1 y_3+54 t^2 x_2 y_3-18 t^2 x_4 y_3+18 t^2 x_1 y_4 \\ - -36 t^2 x_2 y_4+18 t^2 x_3 y_4+36 t x_2 y_1-54 t x_3 y_1+18 t x_4 y_1-36 t x_1 y_2 \\ - +54 t x_3 y_2-18 t x_4 y_2+54 t x_1 y_3-54 t x_2 y_3-18 t x_1 y_4+18 t x_2 y_4 \\ - -18 x_2 y_1+18 x_3 y_1+18 x_1 y_2-18 x_3 y_2-18 x_1 y_3+18 x_2 y_3 -\end{array} -\]

That is... unwieldy. So, we note that there are a lot of terms that involve multiplications involving x1, y1, and y4, which would all disappear if we axis-align our curve, which is why aligning is a great idea.

-
-

Aligning our curve so that three of the eight coefficients become zero, we end up with the following simple term function for C(t):

-\[ - 18 \left ( (3 x_3 y_2+2 x_4 y_2+3 x_2 y_3-x_4 y_3)t^2 + (3 x_3 y_2-x_4 y_2-3 x_2 y_3)t + (x_2 y_3-x_3 y_2) \right ) -\]

That's a lot easier to work with: we see a fair number of terms that we can compute and then cache, giving us the following simplification:

-\[ - \left.\begin{matrix} - a = x_3 \cdot y_2 \\ - b = x_4 \cdot y_2 \\ - c = x_2 \cdot y_3 \\ - d = x_4 \cdot y_3 - \end{matrix}\right\} - \ C(t) = 18 \cdot \left ( (-3a + 2b + 3c - d)t^2 + (3a - b - 3c)t + (c - a) \right ) -\]

This is a plain quadratic curve, and we know how to solve C(t) = 0; we use the quadratic formula:

-\[ - \left.\begin{matrix} - x =& 18(-3a + 2b + 3c - d) \\ - y =& 18(3a - b - 3c) \\ - z =& 18(c - a) - \end{matrix}\right\} - \ C(t) = 0 \ \Rightarrow\ t = \frac{-y \pm \sqrt{y^2 - 4 x z}}{2x} -\]

We can easily compute this value if the descriminator isn't a negative number (because we only want real roots, not complex roots), and if x is not zero, because divisions by zero are rather useless.

-

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.

- -
; } - - }, - "canonical": { - "locale": "en-GB", - "title": "Canonical form (for cubic curves)", - "getContent": function(handler) { return
- -

While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve. As a curvature controlled by more than one control points, it exhibits all kinds of features like loops, cusps, odd colinear features, and up to two inflection points because the curvature can change direction up to three times. Now, knowing what kind of curve we're dealing with means that some algorithms can be run more efficiently than if we have to implement them as generic solvers, so is there a way to determine the curve type without lots of work?

-

As it so happens, the answer is yes and the solution we're going to look at was presented by Maureen C. Stone from Xerox PARC and Tony D. deRose from the University of Washington in their joint paper "A Geometric Characterization of Parametric Cubic curves". It was published in 1989, and defines curves as having a "canonical" form (i.e. a form that all curves can be reduced to) from which we can immediately tell which features a curve will have. So how does it work?

-

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:

- -

This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0), (0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have certain features. Namely, if the fourth point is...

-
    -
  1. anywhere on and in the red zone, the curve will be self-intersecting, yielding either a cusp or a loop. Anywhere inside the the red zone, this will be a loop. We won't know where that loop is (in terms of t values), but we are guaranteed that there is one.
  2. -
  3. on the left (red) edge, the curve will have a cusp. We again don't know where, just that it -has one. This edge is described by the function:
  4. -
-\[ - y = \frac{-x^2 + 2x + 3}{4}, \{ x \leq 1 \} - \]
    -
  1. on the lower right (pink) edge, the curve will have a loop at t=1, so we know the end coordinate of -the curve also lies on the curve. This edge is described by the function:
  2. -
-\[ - y = \frac{\sqrt{3(4x - x^2)} - x}{2}, \{ 0 \leq x \leq 1 \} - \]
    -
  1. on the top (blue) edge, the curve will have a loop at t=0, so we know the start coordinate of -the curve also lies on the curve. This edge is described by the function:
  2. -
-\[ - y = \frac{-x^2 + 3x}{3}, \{ x \leq 0 \} - \]
    -
  1. inside the green zone, the curve will have a single inflection, switching concave/convex once.
  2. -
  3. between the red and green zones, the curve has two inflections, meaning its curvature switches between -concave/convex form twice.
  4. -
  5. anywhere on the right of the red zone, the curve will have no inflections. It'll just be a well-behaved arch.
  6. -
-

Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.

- -
-

Wait, where do those lines come from?

-

Without repeating the paper mentioned at the top of this section, the loop-boundaries come from rewriting the curve into canonical form, and then solving the formulae for which constraints must hold for which possible curve properties. In the paper these functions yield formulae for where you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived for the full cubic expression, meaning they apply to t=-∞ to t=∞... For Bézier curves we only care about the "clipped interval" t=0 to t=1, so some of the properties that apply when you look at the curve over an infinite interval simply don't apply to the Bézier curve interval.

-

The right bound for the loop region, indicating where the curve switches from "having inflections" to "having a loop", for the general cubic curve, is actually mirrored over x=1, but for Bézier curves this right half doesn't apply, so we don't need to pay attention to it. Similarly, the boundaries for t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general curve does over the interval t=0 to t=1.

-

For the full details, head over to the paper and read through sections 3 and 4. If you still remember your high school precalculus, you can probably follow along with this paper, although you might have to read it a few times before all the bits "click".

-
-

So now the question becomes: how do we manipulate our curve so that it fits this canonical form, with three fixed points, and one "free" point? Enter linear algerba. Don't worry, I'll be doing all the math for you, as well as show you what the effect is on our curves, but basically we're going to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus approach is very hard to work with, when the equivalent geometrical solution is super obvious.

-

The approach is going to start with a curve that doesn't have all-colinear points (so we need to make sure the points don't all fall on a straight line), and then applying four graphics operations that you will probably have heard of: translation (moving all points by some fixed x- and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an operation that turns rectangles into parallelograms).

-

Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going to make use of an interesting trick here, by pretending our 2D coordinates are 3D, with the z coordinate simply always being 1. This is an old trick in graphics to overcome the limitations of 2D transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form (ax + by, cx + dy), which means we can't do translation, since that requires we end up with some kind of (x + a, y + b). If we add a bogus z coordinate that is always 1, then we can suddenly add arbitrary values. For example:

-\[ -\left [ \begin{array} - 01 & 0 & a \\ - 0 & 1 & b \\ - 0 & 0 & 1 - \end{array} \right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - z=1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - 1 \cdot x + 0 \cdot y + a \cdot z \\ - 0 \cdot x + 1 \cdot y + b \cdot z \\ - 0 \cdot x + 0 \cdot y + 1 \cdot z - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + a \cdot 1 \\ - y + b \cdot 1 \\ - 1 \cdot z - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + a \\ - y + b \\ - z=1 - \end{matrix} -\right ] -\]

Sweet! z stays 1, so we can effectively ignore it entirely, but we added some plain values to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:

-\[ -T_1 = -\left [ \begin{array} - 01 & 0 & -{P_1}_x \\ - 0 & 1 & -{P_1}_y \\ - 0 & 0 & 1 - \end{array} \right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - 1 \cdot x + 0 \cdot y - {P_1}_x \cdot 1 \\ - 0 \cdot x + 1 \cdot y - {P_1}_y \cdot 1 \\ - 0 \cdot x + 0 \cdot y + 1 \cdot 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x - {P_1}_x \\ - y - {P_1}_y \\ - 1 - \end{matrix} -\right ] -\]

Running all our coordinates through this transformation gives a new set of coordinates, let's call those U, where the first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the x=0 line, so what we want is a transformation matrix that, when we run it, subtracts x from whatever x we currently have. This is called shearing, and the typical x-shear matrix and its transformation looks like this:

-\[ -\left [ - \begin{matrix} - 1 & S & 0 \\ - 0 & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\cdot -\left [ - \begin{matrix} - x \\ - y \\ - 1 - \end{matrix} -\right ] -= -\left [ - \begin{matrix} - x + S \cdot y \\ - y \\ - 1 - \end{matrix} -\right ] -\]

So we want some shearing value that, when multiplied by y, yields -x, so our x coordinate becomes zero. That value is simpy -x/y, because -x/y * y = -x. Done:

-\[ -T_2 = -\left [ - \begin{matrix} - 1 & -\frac{ {U_2}_x }{ {U_2}_y } & 0 \\ - 0 & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on (0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to do some scaling to make sure it ends up at (0,1). Additionally, we want point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3x, and y-scaling by point2y. This is really easy:

-\[ -T_3 = -\left [ - \begin{matrix} - \frac{1}{ {V_3}_x } & 0 & 0 \\ - 0 & \frac{1}{ {V_2}_y } & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared point 2 earlier. In this case, we do the same trick, but with y/x rather than x/y because we're not x-shearing but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:

-\[ -T_4 = -\left [ - \begin{matrix} - 1 & 0 & 0 \\ - \frac{1 - {W_3}_y}{ {W_3}_x } & 1 & 0 \\ - 0 & 0 & 1 - \end{matrix} -\right ] -\]

And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:

-\[ -mapped_4 = \left ( - \begin{matrix} - x = \left ( - \frac - { - -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} - } - { - -x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2} - } - \right ) -\\ - y = \left ( - \frac{(-y_1+y_4)}{-y_1+y_2} - + - \frac - { - \left ( 1 - \frac{-y_1+y_3}{-y_1+y_2} \right ) - \left ( -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} \right ) - } - { - -x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2} - } - \right ) - \end{matrix} -\right ) -\]

That looks very complex, but notice that every coordinate value is being offset by the initial translation, and a lot of terms in there repeat: it's pretty easy to calculate this fast, since there's so much we can cache and reuse while we compute this mapped coordinate!

-

First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?

-\[ -... = \left ( - \begin{matrix} - x = \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right ) -\\ - y = - \frac{y_4}{y_2} - + - \left ( 1 - \frac{y_3}{y_2} \right ) - \cdot - \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right ) - \end{matrix} -\right ) -\]

Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate it. In fact, let's pull out all those common factors to see just how simple this is:

-\[ -... = \left ( - \begin{matrix} - x = (x_4 - x_2 \cdot f_{42}) / ( x_3- x_2 \cdot f_{32} ) -\\ - y = - f_{42} - + - \left ( 1 - f_{32} \right ) - \cdot - x - \end{matrix} -\right ), f_{32} = \frac{y_3}{y_2}, f_{42} = \frac{y_4}{y_2} -\]

That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it look!

- -
-

How do you track all that?

-

Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply use Mathematica. Tracking all this math by hand is insane, and we invented computers, literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the program do the math for me. And real math, too, with symbols, not with numbers. In fact, here's the Mathematica notebook if you want to see how this works for yourself.

-

Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's $295 for home use, but it's also free when you buy a $35 raspberry pi. Obviously, I bought a raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to do, Mathematica can just do it for you. And we don't have to be geniusses to work out what the maths looks like. That's what we have computers for.

-
-

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:

- -
; } - - }, - "arclength": { - "locale": "en-GB", - "title": "Unknown title (arclength)", - "getContent": function(handler) { return
-
; } - - }, - "arclengthapprox": { - "locale": "en-GB", - "title": "Unknown title (arclengthapprox)", - "getContent": function(handler) { return
-
; } - - }, - "tracing": { - "locale": "en-GB", - "title": "Unknown title (tracing)", - "getContent": function(handler) { return
-
; } - - }, - "intersections": { - "locale": "en-GB", - "title": "Unknown title (intersections)", - "getContent": function(handler) { return
-
; } - - }, - "curveintersection": { - "locale": "en-GB", - "title": "Unknown title (curveintersection)", - "getContent": function(handler) { return
-
; } - - }, - "abc": { - "locale": "en-GB", - "title": "Unknown title (abc)", - "getContent": function(handler) { return
-
; } - - }, - "moulding": { - "locale": "en-GB", - "title": "Unknown title (moulding)", - "getContent": function(handler) { return
-
; } - - }, - "pointcurves": { - "locale": "en-GB", - "title": "Unknown title (pointcurves)", - "getContent": function(handler) { return
-
; } - - }, - "catmullconv": { - "locale": "en-GB", - "title": "Unknown title (catmullconv)", - "getContent": function(handler) { return
-
; } - - }, - "catmullmoulding": { - "locale": "en-GB", - "title": "Unknown title (catmullmoulding)", - "getContent": function(handler) { return
-
; } - - }, - "polybezier": { - "locale": "en-GB", - "title": "Unknown title (polybezier)", - "getContent": function(handler) { return
-
; } - - }, - "shapes": { - "locale": "en-GB", - "title": "Unknown title (shapes)", - "getContent": function(handler) { return
-
; } - - }, - "projections": { - "locale": "en-GB", - "title": "Unknown title (projections)", - "getContent": function(handler) { return
-
; } - - }, - "offsetting": { - "locale": "en-GB", - "title": "Unknown title (offsetting)", - "getContent": function(handler) { return
-
; } - - }, - "graduatedoffset": { - "locale": "en-GB", - "title": "Unknown title (graduatedoffset)", - "getContent": function(handler) { return
-
; } - - }, - "circles": { - "locale": "en-GB", - "title": "Unknown title (circles)", - "getContent": function(handler) { return
-
; } - - }, - "circles_cubic": { - "locale": "en-GB", - "title": "Unknown title (circles_cubic)", - "getContent": function(handler) { return
-
; } - - }, - "arcapproximation": { - "locale": "en-GB", - "title": "Unknown title (arcapproximation)", - "getContent": function(handler) { return
-
; } - - }, - "bsplines": { - "locale": "en-GB", - "title": "Unknown title (bsplines)", - "getContent": function(handler) { return
-
; } - - }, - "comments": { - "locale": "en-GB", - "title": "Unknown title (comments)", - "getContent": function(handler) { return
-
; } - - }, - "locale-switcher": { - "locale": "en-GB", - "title": "locale-switcher", - "getContent": function(handler) { return
-

Read this in your own language:

- -

Don't see your language listed? Help translate this content! -

-
; } - - } -};