diff --git a/docs/fonts/roboto-v20-latin-regular.woff b/docs/fonts/roboto-v20-latin-regular.woff new file mode 100644 index 00000000..69c88254 Binary files /dev/null and b/docs/fonts/roboto-v20-latin-regular.woff differ diff --git a/docs/index.html b/docs/index.html index e77526c3..6b6bf89f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -11,6 +11,13 @@ + + @@ -31,7 +38,7 @@ - + @@ -124,9 +131,9 @@
Read this in your own language:

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

diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 26a6f398..eaede16d 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -13,6 +13,14 @@ + + @@ -33,7 +41,7 @@ - + @@ -126,9 +134,9 @@
Read this in your own language:

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

diff --git a/docs/news/2020-09-18.html b/docs/news/2020-09-18.html index e8f53796..2473bd03 100644 --- a/docs/news/2020-09-18.html +++ b/docs/news/2020-09-18.html @@ -13,6 +13,13 @@ + + @@ -27,7 +34,7 @@ - + diff --git a/docs/news/index.html b/docs/news/index.html index ae73eabf..2b117f51 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -13,6 +13,13 @@ + + @@ -26,7 +33,7 @@ - + diff --git a/docs/news/rss.xml b/docs/news/rss.xml index f54f28e9..60cbccc1 100644 --- a/docs/news/rss.xml +++ b/docs/news/rss.xml @@ -6,7 +6,7 @@ News updates for the primer on Bézier Curves by Pomax en-GB - Sun Oct 18 2020 14:26:48 +00:00 + Tue Oct 27 2020 18:29:36 +00:00 https://pomax.github.io/bezierinfo/images/og-image.png A Primer on Bézier Curves diff --git a/docs/style.css b/docs/style.css index 3f386890..6bea6bd0 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,20 +1,3 @@ -:root[lang="en-GB"] { - font-family: "Museo", "Helvetica Neue", "Helvetica", Arial, sans-serif; - font-size: 18px; -} - -:root[lang="ja-JP"] { - font-family: "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "Osaka", "メイリオ", "Meiryo", "MS Pゴシック", "MS PGothic", "Helvetica Neue", - "Helvetica", Arial, sans-serif; - font-size: 15.6px; -} - -:root[lang="zh-CN"] { - font-family: "华文细黑", "STXihei", "PingFang TC", "微软雅黑体", "Microsoft YaHei New", "微软雅黑", "Microsoft Yahei", "宋体", "SimSun", - "Helvetica Neue", "Helvetica", Arial, sans-serif; - font-size: 16.7px; -} - :root { background: #eee; background-image: url(images/paper.png); @@ -111,9 +94,15 @@ ul.lang-switcher li { } ul.lang-switcher + p { font-size: 85%; - margin: 0; + margin-bottom: 2em; } +ul.lang-switcher li .localisation-progress { + font-size: 60%; + vertical-align: middle; +} + +/* @media screen and (min-width: 992px) { ul.lang-switcher + p { display: inline; @@ -121,6 +110,7 @@ ul.lang-switcher + p { font-size: 75%; } } +*/ /* --------------------- */ diff --git a/docs/uk-UA/index.html b/docs/uk-UA/index.html new file mode 100644 index 00000000..22ce62bd --- /dev/null +++ b/docs/uk-UA/index.html @@ -0,0 +1,9413 @@ + + + + + + Підручник з кривих Безьє + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ This page on GitHub + + This page on GitHub + +
+ +
+ + + submit to reddit + submit to hacker news + tweet your read + +
+ + + +
+

+ Підручник з кривих Безьє +

+

Безкоштовна онлайн-книга, яка навчить вас всьому необхідному, щоб працювати з кривими Безьє.

+
+ Читати рідною мовою: + +

(Не бачите вашої мови у списку? Допоможіть перекласти цей контент!)

+
+ +

+ Ласкаво прошу до Підручника з кривих Безьє. Це безкоштовний вебсайт/електронна книга, що пояснює і математичні, і програмувальні аспекти + кривих Безьє, покриваючи широкий спектр областей, які стосуються малювання та роботи з цими кривими. Криві Безьє застосовуються всюди, + починаючи з кривих у Photoshop, і закінчуючи функціями пом'якшення (easing function) CSS та описом контурів популярних шрифтів. +

+

+ Якщо ви тут вперше: ласкаво прошу! Напишіть мені сюди, якщо вас цікавить щось, + пов'язане з кривими Безьє, чого немає у підручнику. +

+ +

+ Якщо ви використовуєте цей ресурс для наукових досліджень, або пишете власне програмне забезпечення, будь ласка, розгляньте можливість + фінансової підтримки + (будь-яка сума вітається), або станьте патроном на Patreon. Мені не платять за цю роботу, + тому якщо ви вважаєте цей сайт корисним, і хотіли б, щоб він підтримувався протягом тривалого часу, знайте: багато кави потрібно було за ці + роки, і ще багато буде потрібно. Тому, якщо можете допомогти з кавою, то можете бути впевнені, що цей ресурс буде підтримуватися ще довго! be + helping keep a resource alive and well! +

+ +

Pomax

+ + +
+ +
+
+

Preface

+

+ 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 are 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 making + them known to the world as a curve well-suited for design work (publishing his investigations in 1962 while working for Renault), 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, as he began investigating the nature of these + curves in 1959 while working at Citroën, and came 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, whose publications on them date back at + least as far 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 Inkscape, those curves you've been drawing are Bézier curves. +

+

+ But 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 to do with these curves? That's + what this page is for. Prepare to be mathed! +

+
+

Virtually all Bézier graphics are interactive.

+

+ This page uses interactive examples, relying heavily on Bezier.js, as well as maths + formulae which are typeset into SVG using the XeLaTeX typesetting system and + pdf2svg by David Barton. +

+

This book is open source.

+

+ This book is an open source software project, and lives on two github repositories. 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 code that gets turned into the web version, and is also where you should file + issues if you find bugs or have ideas on what to change or add to the primer. +

+

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

+

What language is all this example code in?

+

+ There are way too many programming languages to favour one of all others, soo all the example code in this Primer uses a form of + pseudo-code that uses a syntax that's close enough to, but not actually, modern scripting languages like JS, Python, etc. That means you + won't be able to copy-paste any of it without giving it any thought, but that's intentional: if you're reading this primer, presumably you + want to learn, and you don't learn by copy-pasting. You learn by doing things yourself, making mistakes, and then fixing + those mistakes. Now, of course, I didn't intentionally add errors in the example code just to trick you into making mistakes (that would + be horrible!) but I did intentionally keep the code from favouring one programming language over another. Don't worry though, if + you know even a single procedural programming language, you should be able to read the examples without any difficulties. +

+

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

+

Help support the book!

+

+ 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 have two options: you can either head on over to the + Patreon page for this book, or if you prefer to make a one-time donation, head on over to + the buy Pomax a coffee page. This + work has grown from a small primer to a 100-plus print-page-equivalent reader on the subject of Bézier curves over the years, 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! +

+
+
+
+

Що нового?

+

+ Цей підручник постійно розвививається, тож залежно від того, коли ви востаннє його переглядали, тут можуть бути оновлення. Перейдіть за + цим посиланням, щоб побачити, що було додано. (Також доступний RSS-канал) +

+ + + +
+

August-September 2020

+
    +
  • +

    + Completely overhauled the site: the Primer is now a normal web page that works fine with JS disabled, but obviously better with JS + turned on. +

    +
  • +
+

June 2020

+
    +
  • Added automatic CI/CD using Github Actions

  • +
+

January 2020

+
    +
  • Added reset buttons to all graphics

  • +
  • Updated to preface to correctly describe the on-page maths

  • +
  • Fixed the Catmull-Rom section because it had glaring maths errors

  • +
+

August 2019

+
    +
  • Added a section on (plain) rational Bezier curves

  • +
  • Improved the Graphic component to allow for sliders

  • +
+

December 2018

+
    +
  • Added a section on curvature and calculating kappa.

  • +
  • +

    + Added a Patreon page! Head on over to patreon.com/bezierinfo to help support this + site! +

    +
  • +
+

August 2018

+
    +
  • Added a section on finding a curve's y, if all you have is the x coordinate.

  • +
+

July 2018

+
    +
  • Rewrote the 3D normals section, implementing and explaining Rotation Minimising Frames.

  • +
  • Updated the section on curve order raising/lowering, showing how to get a least-squares optimized lower order curve.

  • +
  • +

    (Finally) updated 'npm test' so that it automatically rebuilds when files are changed while the dev server is running.

    +
  • +
+

June 2018

+
    +
  • Added a section on direct curve fitting.

  • +
  • Added source links for all graphics.

  • +
  • Added this "What's new?" section.

  • +
+

April 2017

+
    +
  • Added a section on 3d normals.

  • +
  • Added live-updating for the social link buttons, so they always link to the specific section you're reading.

  • +
+

February 2017

+
    +
  • Finished rewriting the entire codebase for localization.

  • +
+

January 2016

+
    +
  • Added a section to explain the Bezier interval.

  • +
  • Rewrote the Primer as a React application.

  • +
+

December 2015

+
    +
  • Set up the split repository between BezierInfo-2 as development repository, and bezierinfo as live page.

  • +
  • +

    + Removed the need for client-side LaTeX parsing entirely, so the site doesn't take a full minute or more to load all the graphics. +

    +
  • +
+

May 2015

+
    +
  • Switched over to pure JS rather than Processing-through-Processing.js

  • +
  • Added Cardano's algorithm for finding the roots of a cubic polynomial.

  • +
+

April 2015

+
    +
  • Added a section on arc length approximations.

  • +
+

February 2015

+
    +
  • Added a section on the canonical cubic Bezier form.

  • +
+

November 2014

+
    +
  • Switched to HTTPS.

  • +
+

July 2014

+
    +
  • Added the section on arc approximation.

  • +
+

April 2014

+
    +
  • Added the section on Catmull-Rom fitting.

  • +
+

November 2013

+
    +
  • Added the section on Catmull-Rom / Bezier conversion.

  • +
  • Added the section on Bezier cuves as matrices.

  • +
+

April 2013

+
    +
  • Added a section on poly-Beziers.

  • +
  • Added a section on boolean shape operations.

  • +
+

March 2013

+
    +
  • First drastic rewrite.

  • +
  • Added sections on circle approximations.

  • +
  • Added a section on projecting a point onto a curve.

  • +
  • Added a section on tangents and normals.

  • +
  • Added Legendre-Gauss numerical data tables.

  • +
+

October 2011

+
    +
  • +

    + First commit for the bezierinfo site, based on the pre-Primer webpage that covered + the basics of Bezier curves in HTML with Processing.js examples. +

    +
  • +
+
+
+
+
+

+ + Короткий вступ +

+

+ Давайте розпочнемо з добрих новин: криві Безьє, про які ми говоритимемо, ви зможете побачити далі на графіках. Ці криві розпочинаються у + якійсь певній точці, і закінчуються у якійсь певній точці. Їх кривизна залежить від однієї або кількох "проміжних" контрольних точок. + Зараз, оскільки всі графіки на цій сторінці інтерактивні, поекспериментуйте трохи з цими кривими. Клікніть на точку мишкою й потягніть - + так ви зможете відчути, як форма кривої змінюється в залежності від ваших дій. +

+
+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +
+ +

+ Ці криві інтенсивно використовуються у системах автоматизованого проектування та виробництва (CAD/CAM), а також у програмах для графічного + дизайну, таких як Adobe Illustrator, Photoshop, Inkscape, GIMP, тощо. Також криві Безьє використовуються у графічних технологіях, таких як + масштабована векторна графіка (SVG) та шрифти OpenType (TTF/OTF). Криві Безьє використовуються багато де, тому якщо хочете дізнатись про + них більше, приготуйтесь трохи повчитися! +

+
+
+

+ + So what makes a Bézier Curve? +

+

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

+

+ Bézier 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: +

+ + +

+ So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or + decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over + those lines gives us two points, between which we can again perform linear interpolation, yielding a single point. And that point —and all + points we can form in this way for all ratios taken together— form our Bézier curve: +

+ + + Скрипти вимкнено + + + + + +

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, for instance, that a curve will never extend beyond the points we used to construct it). +

+

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

+
+
+

+ + The mathematics of Bézier curves +

+

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + + Скрипти вимкнено + + + + + +

+ Bézier curves are just one out of the many classes of parametric functions, and are characterised by using the same base function for all + of the output values. In the example we saw above, the x and y values were generated by different functions (one uses a + sine, the other a cosine); but Bézier curves use the "binomial polynomial" for both the x and y outputs. So what are + binomial polynomials? +

+

You may remember polynomials from high school. They're those sums that look like this:

+ + +

+ If the highest order term they have is , 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 being fixed 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: +

+ + +

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

+ + +

+ 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", giving us a simple number sequence known as + Pascal's triangle. 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: +

+ + +

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

+ + +

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

+ + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
+ +

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ +

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

+ + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
+ +

+ Perfect. Of course, we can optimize further. For most computer graphics purposes, we don't need arbitrary curves (although we will also + provide code for arbitrary curves in this primer); we need quadratic and cubic curves, and that means we can drastically simplify the + code: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
+ +

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

+
+ +

So, now we know what the basis function looks like, time to add in the magic that makes Bézier curves so special: control points.

+
+
+

+ + Controlling Bézier curvatures +

+

+ Bézier curves are, like all "splines", interpolation functions. This means that 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-and-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 straightforward 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: +

+ + +

+ 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 (110,150), is controlled by (25,190) and (210,250) and ends at + (210,30), we use this Bézier curve: +

+ + +

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:

+ + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
+ +

And now for the optimized versions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
+ +

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

+
+
+
+

+ + Controlling Bézier curvatures, part 2: Rational Béziers +

+

+ We can further control Bézier curves by "rationalising" them: that is, adding a "ratio" value in addition to the weight value discussed in + the previous section, thereby gaining control over "how strongly" each coordinate influences the curve. +

+

Adding these ratio values to the regular Bézier curve function is fairly easy. Where the regular function is the following:

+ + +

The function for rational Bézier curves has two more terms:

+ + +

+ In this, the first new term represents an additional weight for each coordinate. For example, if our ratio values are [1, 0.5, 0.5, 1] + then ratio0 = 1, ratio1 = 0.5, and so on, and is effectively identical as if we were just + using different weight. So far, nothing too special. +

+

+ However, the second new term is what makes the difference: every point on the curve isn't just a "double weighted" point, it is a + fraction of the "doubly weighted" value we compute by introducing that ratio. When computing points on the curve, we compute the + "normal" Bézier value and then divide that by the Bézier value for the curve that only uses ratios, not weights. +

+

+ This does something unexpected: it turns our polynomial into something that isn't a polynomial anymore. It is now a kind of curve + that is a super class of the polynomials, and can do some really cool things that Bézier curves can't do "on their own", such as perfectly + describing circles (which we'll see in a later section is literally impossible using standard Bézier curves). +

+

+ But the best way to show what this does is to do literally that: let's look at the effect of "rationalising" our Bézier curves using an + interactive graphic for a rationalised curves. The following graphic shows the Bézier curve from the previous section, "enriched" with + ratio factors for each coordinate. The closer to zero we set one or more terms, the less relative influence the associated coordinate + exerts on the curve (and of course the higher we set them, the more influence they have). Try to change the values and see how it affects + what gets drawn: +

+ + + Скрипти вимкнено + + + + + + + + +

+ You can think of the ratio values as each coordinate's "gravity": the higher the gravity, the closer to that coordinate the curve will + want to be. You'll also notice that if you simply increase or decrease all the ratios by the same amount, nothing changes... much like + with gravity, if the relative strengths stay the same, nothing really changes. The values define each coordinate's influence + relative to all other points. +

+
+

How to implement rational curves

+

Extending the code of the previous section to include ratios is almost trivial:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ +

And that's all we have to do.

+
+
+
+

+ + The Bézier interval [0,1] +

+

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

+ + +

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

+ + +

+ 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, which is based on 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 of 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; rather than fixing the + interval, and giving you freedom to choose the 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. It has even been used in font design, for example for the Inconsolata typeface. +

+
+
+

+ + Bézier curvatures as matrix operations +

+

+ We can also represent Bézier curves as matrix operations, by expressing the Bézier formula as a polynomial basis function and a + coefficients matrix, and the actual coordinates as a matrix. Let's look at what this means for the cubic curve, using P... to + refer to coordinate values "in one or more dimensions": +

+ + +

Disregarding our actual coordinates for a moment, we have:

+ + +

We can write this as a sum of four expressions:

+ + +

And we can expand these expressions:

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+
+
+

+ + de Casteljau's algorithm +

+

+ 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 + at each value, getting the x/y values we need to plot. Unfortunately, the more complex the curve gets, the more expensive + this computation becomes. Instead, we can use de Casteljau's algorithm to draw curves. This is a geometric approach to curve + drawing, and it's 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. +
  • +
+

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

+ + + Скрипти вимкнено + + + + + +
+

How to implement de Casteljau's algorithm

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
+ +

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
+ +

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

+
+
+
+

+ + Simplified drawing +

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
+ +

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

+ + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
+ +

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

+
+
+
+

+ + Splitting curves +

+

+ Using de Casteljau's algorithm, we can also find all the points we need to split up a Bézier curve into two, smaller curves, which taken + together form the original curve. When we construct de Casteljau's skeleton for some value t, the procedure gives us all the + points we need to split a curve at that t value: one curve is defined by all the inside skeleton points found prior to our + on-curve point, with the other curve being defined by all the inside skeleton points after our on-curve point. +

+ + + Скрипти вимкнено + + + + + +
+

implementing curve splitting

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+ +

+ 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". These new curves will have the + same order as the original curve, and can be overlaid exactly on the original curve. +

+
+
+
+

+ + Splitting curves using matrices +

+

+ 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: (we'll reverse the Bézier coefficients vector for legibility) +

+ + +

and

+ + +

+ 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 separate out the actual + "point on the curve" information into a new matrix multiplication: +

+ + +

and

+ + +

+ If we could compact these matrices back to the form [t values] · [Bézier 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: +

+ + + + + + + + +

+ We can do this because [M · M-1] is the identity matrix. It's 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. In the same way, multiplying our + matrix by [M · M-1] 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: +

+ + +

Excellent! Now we can form our new quadratic curve:

+ + + + + + +

+ 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. 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 that in the previous calculation, we actually evaluated the general interval [0,z]. We were able to + write it down in a more simple form because of the zero, but what we actually evaluated, making the zero explicit, was: +

+ + + + +

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

+ + + + +

+ We're going to do the same trick of multiplying by the identity matrix, to turn [something · M] into + [M · something]: +

+ + +

So, our final second curve looks like:

+ + + + + + +

+ Nice. We see the same thing as before: we 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 this time 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: +

+ + +

and

+ + +

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

+ + +

and

+ + +

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

+
+
+

+ + Lowering and elevating curve order +

+

+ 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 curve with four points that exactly reproduces the original curve. First, 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". 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): +

+ + +

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

+

+ However, there is a surprisingly good way to ensure that a lower order curve looks "as close as reasonably possible" to the original + curve: we can optimise the "least-squares distance" between the original curve and the lower order curve, in a single operation (also + explained over on Sirver's Castle). However, to + use it, we'll need to do some calculus work and then switch over to linear algebra. As mentioned in the section on matrix representations, + some things can be done much more easily with matrices than with calculus functions, and this is one of those things. So... let's go! +

+

We start by taking the standard Bézier function, and condensing it a little:

+ + +

+ Then, we apply one of those silly (actually, super useful) calculus tricks: since our t value is always between zero and one + (inclusive), we know that (1-t) plus t always sums to 1. As such, we can express any value as a sum of + t and 1-t: +

+ + +

+ So, with that seemingly trivial observation, we rewrite that Bézier function by splitting it up into a sum of a (1-t) and + t component: +

+ + +

+ So far so good. Now, to see why we did this, let's write out the (1-t) and t parts, and see what that gives us. + I promise, it's about to make sense. We start with (1-t): +

+ + +

+ So by using this seemingly silly trick, we can suddenly express part of our nth order Bézier function in terms of an (n+1)th + order Bézier function. And that sounds a lot like raising the curve order! Of course we need to be able to repeat that trick for the + t part, but that's not a problem: +

+ + +

+ So, with both of those changed from an order n expression to an order (n+1) expression, we can put them back + together again. Now, where the order n function had a summation from 0 to n, the order n+1 function + uses a summation from 0 to n+1, but this shouldn't be a problem as long as we can add some new terms that "contribute + nothing". In the next section on derivatives, there is a discussion about why "higher terms than there is a binomial for" and "lower than + zero terms" both "contribute nothing". So as long as we can add terms that have the same form as the terms we need, we can just include + them in the summation, they'll sit there and do nothing, and the resulting function stays identical to the lower order curve. +

+

Let's do this:

+ + +

+ And this is where we switch over from calculus to linear algebra, and matrices: we can now express this relation between Bézier(n,t) and + Bézier(n+1,t) as a very simple matrix multiplication: +

+ + +

where the matrix M is an n+1 by n matrix, and looks like:

+ + +

+ That might look unwieldy, but it's really just a mostly-zeroes matrix, with a very simply fraction on the diagonal, and an even simpler + fraction to the left of it. Multiplying a list of coordinates with this matrix means we can plug the resulting transformed coordinates + into the one-order-higher function and get an identical looking curve. +

+

Not too bad!

+

+ Equally interesting, though, is that with this matrix operation established, we can now use an incredibly powerful and ridiculously simple + way to find out a "best fit" way to reverse the operation, called + the normal equation. What it does is minimize the sum of the square + differences between one set of values and another set of values. Specifically, if we can express that as some function + A x = b, we can use it. And as it so happens, that's exactly what we're dealing with, so: +

+ + +

The steps taken here are:

+
    +
  1. We have a function in a form that the normal equation can be used with, so
  2. +
  3. apply the normal equation!
  4. +
  5. + Then, we want to end up with just Bn on the left, so we start by left-multiply both sides such that we'll end up with lots of + stuff on the left that simplified to "a factor 1", which in matrix maths is the + identity matrix. +
  6. +
  7. + In fact, by left-multiplying with the inverse of what was already there, we've effectively "nullified" (but really, one-inified) that + big, unwieldy block into the identity matrix I. So we substitute the mess with I, and then +
  8. +
  9. + because multiplication with the identity matrix does nothing (like multiplying by 1 does nothing in regular algebra), we just drop it. +
  10. +
+

+ And we're done: we now have an expression that lets us approximate an n+1th order curve with a lower nth order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for + raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control + points, and press your up and down arrow keys to raise or lower the curve order. +

+ + + Скрипти вимкнено + + + + + + +
+
+

+ + Derivatives +

+

+ 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 differentiation of a Bézier curve is + relatively straightforward, although we do need a bit of math. +

+

First, let's look at the derivative rule for Bézier curves, which is:

+ + +

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

+ + +

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

+ + +

+ Applying the product and + chain rules gives us: +

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +
+ +

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

+ + + + +

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

+ + +

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

+
+
+

+ + Tangents and normals +

+

+ 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 of travel at specific points, and is literally just the first derivative of our curve: +

+ + +

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

+ + +

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

+ + +
+

+ 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 circle 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 nice and easy computation: +

+ + +

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

+ + +

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

+
+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +
+
+
+

+ + Working with 3D normals +

+

+ Before we move on to the next section we need to spend a little bit of time on the difference between 2D and 3D. While for many things + this difference is irrelevant and the procedures are identical (for instance, getting the 3D tangent is just doing what we do for 2D, but + for x, y, and z, instead of just for x and y), when it comes to normals things are a little more complex, and thus more work. Mind you, + it's not "super hard", but there are more steps involved and we should have a look at those. +

+

+ Getting normals in 3D is in principle the same as in 2D: we take the normalised tangent vector, and then rotate it by a quarter turn. + However, this is where things get that little more complex: we can turn in quite a few directions, since "the normal" in 3D is a plane, + not a single vector, so we basically need to define what "the" normal is in the 3D case. +

+

+ The "naïve" approach is to construct what is known as the + Frenet normal, where we follow a simple recipe that works in + many cases (but does super bizarre things in some others). The idea is that even though there are infinitely many vectors that are + perpendicular to the tangent (i.e. make a 90 degree angle with it), the tangent itself sort of lies on its own plane already: since each + point on the curve (no matter how closely spaced) has its own tangent vector, we can say that each point lies in the same plane as the + local tangent, as well as the tangents "right next to it". +

+

+ Even if that difference in tangent vectors is minute, "any difference" is all we need to find out what that plane is - or rather, what the + vector perpendicular to that plane is. Which is what we need: if we can calculate that vector, and we have the tangent vector that we know + lies on a plane, then we can rotate the tangent vector over the perpendicular, and presto. We have computed the normal using the same + logic we used for the 2D case: "just rotate it 90 degrees". +

+

So let's do that! And in a twist surprise, we can do this in four lines:

+
    +
  • a = normalize(B'(t))
  • +
  • b = normalize(a + B''(t))
  • +
  • r = normalize(b × a)
  • +
  • normal = normalize(r × a)
  • +
+

Let's unpack that a little:

+
    +
  • + We start by taking the normalized vector for the derivative at some point on the + curve. We normalize it so the maths is less work. Less work is good. +
  • +
  • + Then, we compute b which represents what a next point's tangent would be if the curve stopped changing at our point and + just had the same derivative and second derivative from that point on. +
  • +
  • + This lets us find two vectors (the derivative, and the second derivative added to the derivative) that lie on the same plane, which + means we can use them to compute a vector perpendicular to that plane, using an elementary vector operation called the + cross product. (Note that while that operation uses the × operator, it's most + definitely not a multiplication!) The result of that gives us a vector that we can use as the "axis of rotation" for turning the tangent + a quarter circle to get our normal, just like we did in the 2D case. +
  • +
  • + Since the cross product lets us find a vector that is perpendicular to some plane defined by two other vectors, and since the normal + vector should be perpendicular to the plane that the tangent and the axis of rotation lie in, we can use the cross product a second + time, and immediately get our normal vector. +
  • +
+

+ And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can + move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor + position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc: +

+ + + Скрипти вимкнено + + + + + +

+ However, if you've played with that graphic a bit, you might have noticed something odd. The normal seems to "suddenly twist around the + curve" between t=0.65 and t=0.75... Why is it doing that? +

+

+ As it turns out, it's doing that because that's how the maths works, and that's the problem with Frenet normals: while they are + "mathematically correct", they are "practically problematic", and so for any kind of graphics work what we really want is a way to compute + normals that just... look good. +

+

Thankfully, Frenet normals are not our only option.

+

+ Another option is to take a slightly more algorithmic approach and compute a form of + Rotation Minimising Frame + (also known as "parallel transport frame" or "Bishop frame") instead, where a "frame" is a set made up of the tangent, the rotational + axis, and the normal vector, centered on an on-curve point. +

+

+ These type of frames are computed based on "the previous frame", so we cannot simply compute these "on demand" for single points, as we + could for Frenet frames; we have to compute them for the entire curve. Thankfully, the procedure is pretty simple, and can be performed at + the same time that you're building lookup tables for your curve. +

+

+ The idea is to take a starting "tangent/rotation axis/normal" frame at t=0, and then compute what the next frame "should" look like by + applying some rules that yield a good looking next frame. In the case of the RMF paper linked above, those rules are: +

+
    +
  • Take a point on the curve for which we know the RM frame already,
  • +
  • take a next point on the curve for which we don't know the RM frame yet, and
  • +
  • + reflect the known frame onto the next point, by treating the plane through the curve at the point exactly between the next and previous + points as a "mirror". +
  • +
  • + This gives the next point a tangent vector that's essentially pointing in the opposite direction of what it should be, and a normal + that's slightly off-kilter, so: +
  • +
  • + reflect the vectors of our "mirrored frame" a second time, but this time using the plane through the "next point" itself as "mirror". +
  • +
  • Done: the tangent and normal have been fixed, and we have a good looking frame to work with.
  • +
+

So, let's write some code for that!

+
+

Implementing Rotation Minimising Frames

+

+ We first assume we have a function for calculating the Frenet frame at a point, which we already discussed above, inn a way that it + yields a frame with properties: +

+ + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
+ +

Then, we can write a function that generates a sequence of RM frames in the following manner:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
+ +

+ Ignoring comments, this is certainly more code than when we were just computing a single Frenet frame, but it's not a crazy amount more + code to get much better looking normals. +

+
+ +

+ Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising + frames rather than Frenet frames: +

+ + + Скрипти вимкнено + + + + + +

That looks so much better!

+

+ For those reading along with the code: we don't even strictly speaking need a Frenet frame to start with: we could, for instance, treat + the z-axis as our initial axis of rotation, so that our initial normal is (0,0,1) × tangent, and then take things from + there, but having that initial "mathematically correct" frame so that the initial normal seems to line up based on the curve's orientation + in 3D space is just nice. +

+
+
+

+ + Component functions +

+

+ 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 straightforward 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 + leftmost figure is again an interactive curve, without labeled axes (you get coordinates in the graph instead). The center and rightmost + 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; likewise, moving points vertically should only show a + change in the right graph. +

+ + + Скрипти вимкнено + + +

 

+ + + Скрипти вимкнено + + +
+
+

+ + Finding extremities: root finding +

+

+ Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our Bézier curve by finding + maxima and minima on the component functions, by solving the equation B'(t) = 0. We've already seen that the derivative of a Bézier curve + is a simpler Bézier curve, but how do we solve the equality? Fairly easily, actually, until our derivatives are 4th order or higher... + then things get really hard. But let's start simple: +

+

Quadratic curves: linear derivatives.

+

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

+ + +

And then we turn this into our solution for t using basic arithmetics:

+ + +

Done.

+

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

+

Cubic curves: the quadratic formula.

+

+ The derivative of a cubic Bézier curve is a quadratic Bézier curve, and finding the roots for a quadratic polynomial means we can apply + the Quadratic formula. If you've seen it before, you'll remember it, and if + you haven't, it looks like this: +

+ + +

+ So, if we can rewrite the Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic + formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that come out + (because of that plus/minus sign we get two). Any value between 0 and 1 is a root that matters for Bézier curves, anything below or above + that is irrelevant (because Bézier curves are only defined over the interval [0,1]). So, how do we convert? +

+

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

+ + +

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

+ + +

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

+ + +

Easy-peasy. We can now almost trivially find the roots by plugging those values into the quadratic formula.

+

+ And as a cubic curve, there is also a meaningful second derivative, which we can compute by simple taking the derivative of the + derivative. +

+

Quartic curves: Cardano's algorithm.

+

+ We haven't really looked at them before now, but the next step up would be a Quartic curve, a fourth degree Bézier curve. As expected, + these have a derivative that is a cubic function, and now things get much harder. Cubic functions don't have a "simple" rule to find their + roots, like the quadratic formula, and instead require quite a bit of rewriting to a form that we can even start to try to solve. +

+

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

+ + +

+ We can see that the easier formula only has two constants, rather than four, and only two expressions involving t, rather + than three: this makes things considerably easier to solve because it lets us use + regular calculus to find the values that satisfy the equation. +

+

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

+

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

+
+

Implementing Cardano's algorithm for finding all real roots

+

+ 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! +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
+
+ +

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

+

+ And of course, as a quartic curve also has meaningful second and third derivatives, we can quite easily compute those by using the + derivative of the derivative (of the derivative), just as for cubic curves. +

+

Quintic and higher order curves: finding numerical solutions

+

+ And this is where thing stop, because we cannot find the roots for polynomials of degree 5 or higher using algebra (a fact known + as the Abel–Ruffini theorem). Instead, for occasions like these, + where algebra simply cannot yield an answer, we turn to numerical analysis. +

+

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

+

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

+

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

+ + +

(The Wikipedia article has a decent animation for this process, so I will not add a graphic for that here)

+

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

+

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

+

In conclusion:

+

+ So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those + roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red: +

+ + + Скрипти вимкнено + + +

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

+ + + Скрипти вимкнено + + +
+
+

+ + Bounding boxes +

+

+ 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 + axis-aligned 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 curves +

+

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

+ + +

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

+ + +

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

+ + +

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

+ + +

+ We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning + our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae: +

+ + + Скрипти вимкнено + + +

 

+ + + Скрипти вимкнено + + +
+
+

+ + Tight bounding boxes +

+

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

+
+
+

+ + Curve inflections +

+

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

+ + +

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

+ + +

+ The function C(t) is the cross product between 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 evaluating 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: +

+ + +

And of course the same functions for y:

+ + +

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

+ + +

+ 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, and observing that scale does not affect finding + t values, we end up with the following simple term function for C(t): +

+ + +

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

+ + +

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

+ + +

+ We can easily compute this value if the discriminator 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. +

+ + + Скрипти вимкнено + + + +
+
+

+ + The canonical form (for cubic curves) +

+

+ While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve. As a curvature is controlled + by more than one control point, it exhibits all kinds of features like loops, cusps, odd colinear features, and as many as 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 what 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 what the various parts of it mean...

+

+ We see the three fixed points at (0,0), (0,1) and (1,1). The various regions and boundaries indicate what property the original curve will + have, if the fourth point is in/on that region or boundary. Specifically, if the fourth point is... +

+
    +
  1. +

    + ...anywhere inside the red zone, but not on its boundaries, the curve will either be self-intersecting (yielding a loop). We won't + know where it self-intersects (in terms of t values), but we are guaranteed that it does. +

    +
  2. +
  3. +

    + ...on the left (red) edge of the red zone, the curve will have a cusp. We again don't know where, but we know there is one. + This edge is described by the function: +

    + + +
  4. +
  5. +

    + ...on the almost circular, lower right (pink) edge, the curve's end point touches the curve, forming a loop. This edge is described by + the function: +

    + + +
  6. +
  7. +

    ...on the top (blue) edge, the curve's start point touches the curve, forming a loop. This edge is described by the function:

    + + +
  8. +
  9. +

    ...inside the lower (green) zone, past y=1, the curve will have a single inflection (switching concave/convex once).

    +
  10. +
  11. +

    + ...between the left and lower boundaries (below the cusp line but above the single-inflection line), the curve will have two + inflections (switching from concave to convex and then back again, or from convex to concave and then back again). +

    +
  12. +
  13. ...anywhere on the right of self-intersection zone, the curve will have no inflections. It'll just be a simple arch.

  14. +
+

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 pre-calculus, 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 algebra. 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 three 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: +

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

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

+ + +

+ Okay, well, that looks plain ridiculous, but: notice that every coordinate value is being offset by the initial translation, and also + notice that a lot of terms in that expression are repeated. Even though the maths looks crazy as a single expression, we can just + pull this apart a little and end up with an easy-to-calculate bit of code! +

+

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

+ + +

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

+ + +

+ 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 + $344 for home use, up from $295 when I original wrote this, 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 geniuses 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: +

+ + + Скрипти вимкнено + + +
+
+

+ + Finding Y, given X +

+

+ One common task that pops up in things like CSS work, or parametric equalizers, or image leveling, or any other number of applications + where Bézier curves are used as control curves in a way that there is really only ever one "y" value associated with one "x" value, you + might want to cut out the middle man, as it were, and compute "y" directly based on "x". After all, the function looks simple enough, + finding the "y" value should be simple too, right? Unfortunately, not really. However, it is possible and as long as you have + some code in place to help, it's not a lot of a work either. +

+

+ We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" + value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like + it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: + that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds + to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right. +

+ + + Скрипти вимкнено + + + + + +

+ Now, if you look more closely at that right graphic, you'll notice something interesting: if we treat the red line as "the x axis", then + the point where the function crosses our line is really just a root for the cubic function x(t) through a shifted "x-axis"... and + we've already seen how to calculate roots, so let's just run cubic root finding - and not even the complicated + cubic case either: because of the kind of curve we're starting with, we know there is only root, simplifying the code we need! +

+

First, let's look at the function for x(t):

+ + +

+ We can rewrite this to a plain polynomial form, by just fully writing out the expansion and then collecting the polynomial factors, as: +

+ + +

+ Nothing special here: that's a standard cubic polynomial in "power" form (i.e. all the terms are ordered by their power of + t). So, given that a, b, c, d, and x(t) are all + known constants, we can trivially rewrite this (by moving the x(t) across the equal sign) as: +

+ + +

+ You might be wondering "where did all the other 'minus x' for all the other values a, b, c, and d go?" and the answer there is that they + all cancel out, so the only one we actually need to subtract is the one at the end. Handy! So now we just solve this equation using + Cardano's algorithm, and we're left with some rather short code: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
+ +

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

+ + + Скрипти вимкнено + + + + + +
+
+

+ + Arc length +

+

+ How long is a Bézier curve? As it turns out, that's not actually an easy question, because the answer requires maths that —much like root + finding— cannot generally be solved the traditional way. If we have a parametric curve with fx(t) and + fy(t), then the length of the curve, measured from start point to some point t = z, is computed using the + following seemingly straight forward (if a bit overwhelming) formula: +

+ + +

or, more commonly written using Leibnitz notation as:

+ + +

+ This formula says that the length of a parametric curve is in fact equal to the area underneath a function that looks a + remarkable amount like Pythagoras' rule for computing the diagonal of a straight angled triangle. This sounds pretty simple, right? Sadly, + it's far from simple... cutting straight to after the chase is over: for quadratic curves, this formula generates an + unwieldy computation, and we're simply not going to implement things that way. For cubic Bézier curves, things get even more fun, because there is no "closed + form" solution, meaning that due to the way calculus works, there is no generic formula that allows you to calculate the arc length. Let + me just repeat this, because it's fairly crucial: + for cubic and higher Bézier curves, there is no way to solve this function if you want to use it "for all possible coordinates". +

+

Seriously: It cannot be done.

+

+ So we turn to numerical approaches again. The method we'll look at here is the + Gauss quadrature. This approximation + is a really neat trick, because for any nth degree polynomial it finds approximated values for an integral really + efficiently. Explaining this procedure in length is way beyond the scope of this page, so if you're interested in finding out why it + works, I can recommend the University of South Florida video lecture on the procedure, linked in this very paragraph. The general solution + we're looking for is the following: +

+ + +

+ In plain text: an integral function can always be treated as the sum of an (infinite) number of (infinitely thin) rectangular strips + sitting "under" the function's plotted graph. To illustrate this idea, the following graph shows the integral for a sinusoid function. The + more strips we use (and of course the more we use, the thinner they get) the closer we get to the true area under the curve, and thus the + better the approximation: +

+
+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +
+ +

+ Now, infinitely many terms to sum and infinitely thin rectangles are not something that computers can work with, so instead we're going to + approximate the infinite summation by using a sum of a finite number of "just thin" rectangular strips. As long as we use a high enough + number of thin enough rectangular strips, this will give us an approximation that is pretty close to what the real value is. +

+

+ So, the trick is to come up with useful rectangular strips. A naive way is to simply create n strips, all with the same width, + but there is a far better way using special values for C and f(t) depending on the value of n, which indicates + how many strips we'll use, and it's called the Legendre-Gauss quadrature. +

+

+ This approach uses strips that are not spaced evenly, but instead spaces them in a special way based on describing the function + as a polynomial (the more strips, the more accurate the polynomial), and then computing the exact integral for that polynomial. We're + essentially performing arc length computation on a flattened curve, but flattening it based on the intervals dictated by the + Legendre-Gauss solution. +

+
+

+ Note that one requirement for the approach we'll use is that the integral must run from -1 to 1. That's no good, because we're dealing + with Bézier curves, and the length of a section of curve applies to values which run from 0 to "some value smaller than or equal to 1" + (let's call that value z). Thankfully, we can quite easily transform any integral interval to any other integral interval, by + shifting and scaling the inputs. Doing so, we get the following: +

+ + +

+ That may look a bit more complicated, but the fraction involving z is a fixed number, so the summation, and the evaluation of + the f(t) values are still pretty simple. +

+

+ So, what do we need to perform this calculation? For one, we'll need an explicit formula for f(t), because that derivative + notation is handy on paper, but not when we have to implement it. We'll also need to know what these Ci and + ti values should be. Luckily, that's less work because there are actually many tables available that give these + values, for any n, so if we want to approximate our integral with only two terms (which is a bit low, really) then + these tables would tell us that for n=2 we must use the following values: +

+ + +

+ Which means that in order for us to approximate the integral, we must plug these values into the approximate function, which gives us: +

+ + +

+ We can program that pretty easily, provided we have that f(t) available, which we do, as we know the full description for the + Bézier curve functions Bx(t) and By(t). +

+
+ +

+ If we use the Legendre-Gauss values for our C values (thickness for each strip) and t values (location of each strip), + we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, + with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make + a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the + start/end points on the inside? +

+ + + Скрипти вимкнено + + + +
+
+

+ + Approximated arc length +

+

+ Sometimes, we don't actually need the precision of a true arc length, and we can get away with simply computing the approximate arc length + instead. The by far fastest way to do this is to flatten the curve and then simply calculate the linear distance from point to point. This + will come with an error, but this can be made arbitrarily small by increasing the segment count. +

+

+ If we combine the work done in the previous sections on curve flattening and arc length computation, we can implement these with minimal + effort: +

+
+ + + Скрипти вимкнено + + + + + + + + Скрипти вимкнено + + + + + +
+ +

+ You may notice that even though the error in length is actually pretty significant in absolute terms, even at a low number of segments we + get a length that agrees with the true length when it comes to just the integer part of the arc length. Quite often, approximations can + drastically speed things up! +

+
+
+

+ + Curvature of a curve +

+

+ If we have two curves, and we want to line them in up in a way that "looks right", what would we use as metric to let a computer decide + what "looks right" means? +

+

+ For instance, we can start by ensuring that the two curves share an end coordinate, so that there is no "gap" between the end of one and + the start of the next curve, but that won't guarantee that things look right: both curves can be going in wildly different directions, and + the resulting joined geometry will have a corner in it, rather than a smooth transition from one curve to the next. +

+

+ What we want is to ensure that the curvature at the transition from one curve to the + next "looks good". So, we start with a shared coordinate, and then also require that derivatives for both curves match at that coordinate. + That way, we're assured that their tangents line up, which must mean the curve transition is perfectly smooth. We can even make the + second, third, etc. derivatives match up for better and better transitions. +

+

Problem solved!

+

+ However, there's a problem with this approach: if we think about this a little more, we realise that "what a curve looks like" and its + derivative values are pretty much entirely unrelated. After all, the section on reordering curves showed us that + the same looking curve can have an infinite number of curve expressions of arbitrarily high Bézier degree, and each of those will have + wildly different derivative values. +

+

+ So what we really want is some kind of expression that's not based on any particular expression of t, but is based on + something that is invariant to the kind of function(s) we use to draw our curve. And the prime candidate for this is our curve + expression, reparameterised for distance: no matter what order of Bézier curve we use, if we were able to rewrite it as a function of + distance-along-the-curve, all those different degree Bézier functions would end up being the same function for "coordinate at + some distance D along the curve". +

+

We've seen this before... that's the arc length function.

+

+ So you might think that in order to find the curvature of a curve, we now need to solve the arc length function itself, and that this + would be quite a problem because we just saw that there is no way to actually do that. Thankfully, we don't. We only need to know the + form of the arc length function, which we saw above and is fairly simple, rather than needing to solve the arc length + function. If we start with the arc length expression and the + run through the steps necessary to determine its derivative (with an + alternative, shorter demonstration of how to do this found + over on Stackexchange), then the + integral that was giving us so much problems in solving the arc length function disappears entirely (because of the + fundamental theorem of calculus), and what we're left with us + some surprisingly simple maths that relates curvature (denoted as κ, "kappa") to—and this is the truly surprising bit—a specific + combination of derivatives of our original function. +

+

Let me highlight what just happened, because it's pretty special:

+
    +
  1. we wanted to make curves line up, and initially thought to match the curves' derivatives, but
  2. +
  3. that turned out to be a really bad choice, so instead
  4. +
  5. we picked a function that is basically impossible to work with, and then worked with that, which
  6. +
  7. gives us a simple formula that is and expression using the curves' derivatives.
  8. +
+

That's crazy!

+

+ But that's also one of the things that makes maths so powerful: even if your initial ideas are off the mark, you might be much closer than + you thought you were, and the journey from "thinking we're completely wrong" to "actually being remarkably close to being right" is where + we can find a lot of insight. +

+

So, what does the function look like? This:

+ + +

+ Which is really just a "short form" that glosses over the fact that we're dealing with functions of t, so let's expand that a + tiny bit: +

+ + +

+ And while that's a little more verbose, it's still just as simple to work with as the first function: the curvature at some point on any + (and this cannot be overstated: any) curve is a ratio between the first and second derivative cross product, and something that + looks oddly similar to the standard Euclidean distance function. And nothing in these functions is hard to calculate either: for Bézier + curves, simply knowing our curve coordinates means we know what the first and second derivatives are, and so + evaluating this function for any t value is just a matter of basic arithematics. +

+

In fact, let's just implement it right now:

+ + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
+ +

+ That was easy! (Well okay, that "not a number" value will need to be taken into account by downstream code, but that's a reality of + programming anyway) +

+

+ With all of that covered, let's line up some curves! The following graphic gives you two curves that look identical, but use quadratic and + cubic functions, respectively. As you can see, despite their derivatives being necessarily different, their curvature (thanks to being + derived based on maths that "ignores" specific function derivative, and instead gives a formula that smooths out any differences) is + exactly the same. And because of that, we can put them together such that the point where they overlap has the same curvature for both + curves, giving us the smoothest transition. +

+ + + Скрипти вимкнено + + +

+ One thing you may have noticed in this sketch is that sometimes the curvature looks fine, but seems to be pointing in the wrong direction, + making it hard to line up the curves properly. A way around that, of course, is to show the curvature on both sides of the curve, so let's + just do that. But let's take it one step further: we can also compute the associated "radius of curvature", which gives us the implicit + circle that "fits" the curve's curvature at any point, using what is possibly the simplest bit of maths found in this entire primer: +

+ + +

+ So let's revisit the previous graphic with the curvature visualised on both sides of our curves, as well as showing the circle that "fits" + our curve at some point that we can control by using a slider: +

+ + + Скрипти вимкнено + + + + + +
+
+

+ + Tracing a curve at fixed distance intervals +

+

+ Say you want to draw a curve with a dashed line, rather than a solid line, or you want to move something along the curve at fixed distance + intervals over time, like a train along a track, and you want to use Bézier curves. +

+

Now you have a problem.

+

+ The reason you have a problem is that Bézier curves are parametric functions with non-linear behaviour, whereas moving a train along a + track is about as close to a practical example of linear behaviour as you can get. The problem we're faced with is that we can't just pick + t values at some fixed interval and expect the Bézier functions to generate points that are spaced a fixed distance apart. In + fact, let's look at the relation between "distance long a curve" and "t value", by plotting them against one another. +

+

+ The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be + straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To + make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for + this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length + function, which can have more inflection points. +

+ + + Скрипти вимкнено + + +

+ So, how do we "cut up" the arc length function at regular intervals, when we can't really work with it? We basically cheat: we run through + the curve using t values, determine the distance-for-this-t-value at each point we generate during the run, and + then we find "the closest t value that matches some required distance" using those values instead. If we have a low number of + points sampled, we can then even refine which t value "should" work for our desired distance by interpolating between two + points, but if we have a high enough number of samples, we don't even need to bother. +

+

+ So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t + curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks + like, by coloring each section of curve between two distance markers differently: +

+ + + Скрипти вимкнено + + + + + +

Use the slider to increase or decrease the number of equidistant segments used to colour the curve.

+

+ However, are there better ways? One such way is discussed in "Moving Along a Curve with Specified Speed" by David Eberly of Geometric Tools, LLC, but basically because we have no explicit length function (or rather, one we don't have to + constantly compute for different intervals), you may simply be better off with a traditional lookup table (LUT). +

+
+
+

+ + Intersections +

+

+ Let's look at some more things we will want to do with Bézier curves. Almost immediately after figuring out how to get bounding boxes to + work, people tend to run into the problem that even though the minimal bounding box (based on rotation) is tight, it's not sufficient to + perform true collision detection. It's a good first step to make sure there might be a collision (if there is no bounding box + overlap, there can't be one), but in order to do real collision detection we need to know whether or not there's an intersection on the + actual curve. +

+

+ We'll do this in steps, because it's a bit of a journey to get to curve/curve intersection checking. First, let's start simple, by + implementing a line-line intersection checker. While we can solve this the traditional calculus way (determine the functions for both + lines, then compute the intersection by equating them and solving for two unknowns), linear algebra actually offers a nicer solution. +

+

Line-line intersections

+

+ If we have two line segments with two coordinates each, segments A-B and C-D, we can find the intersection of the lines these segments are + an intervals on by linear algebra, using the procedure outlined in this + top coder + article. Of course, we need to make sure that the intersection isn't just on the lines our line segments lie on, but actually on our line + segments themselves. So after we find the intersection, we need to verify that it lies without the bounds of our original line segments. +

+

+ The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on + (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection + point). +

+ + + Скрипти вимкнено + + + +
+

Implementing line-line intersections

+

+ Let's have a look at how to implement a line-line intersection checking function. The basics are covered in the article mentioned above, + but sometimes you need more function signatures, because you might not want to call your function with eight distinct parameters. Maybe + you're using point structs for the line. Let's get coding: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+
+ +

What about curve-line intersections?

+

+ Curve/line intersection is more work, but we've already seen the techniques we need to use in order to perform it: first we + translate/rotate both the line and curve together, in such a way that the line coincides with the x-axis. This will position the curve in + a way that makes it cross the line at points where its y-function is zero. By doing this, the problem of finding intersections between a + curve and a line has now become the problem of performing root finding on our translated/rotated curve, as we already covered in the + section on finding extremities. +

+
+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +
+ +

+ Curve/curve intersection, however, is more complicated. Since we have no straight line to align to, we can't simply align one of the + curves and be left with a simple procedure. Instead, we'll need to apply two techniques we've met before: de Casteljau's algorithm, and + curve splitting. +

+
+
+

+ + Curve/curve intersection +

+

+ Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection finding using a "divide and conquer" + technique: +

+
    +
  1. + Take two curves C1 and C2, and treat them as a pair. +
  2. +
  3. If their bounding boxes overlap, split up each curve into two sub-curves
  4. +
  5. + With C1.1, C1.2, C2.1 and C2.2, form four new pairs (C1.1,C2.1), (C1.1, C2.2), (C1.2,C2.1), and (C1.2,C2.2). +
  6. +
  7. + For each pair, check whether their bounding boxes overlap. +
      +
    1. If their bounding boxes do not overlap, discard the pair, as there is no intersection between this pair of curves.
    2. +
    3. If there is overlap, rerun all steps for this pair.
    4. +
    +
  8. +
  9. + Once the sub-curves we form are so small that they effectively occupy sub-pixel areas, we consider an intersection found, noting that we + might have a cluster of multiple intersections at the sub-pixel level, out of which we pick one to act as "found" t value + (we can either throw all but one away, we can average the cluster's t values, or you can do something even more creative). +
  10. +
+

+ This algorithm will start with a single pair, "balloon" until it runs in parallel for a large number of potential sub-pairs, and then + taper back down as it homes in on intersection coordinates, ending up with as many pairs as there are intersections. +

+

+ The following graphic applies this algorithm to a pair of cubic curves, one step at a time, so you can see the algorithm in action. Click + the button to run a single step in the algorithm, after setting up your curves in some creative arrangement. You can also change the value + that is used in step 5 to determine whether the curves are small enough. Manipulating the curves or changing the threshold will reset the + algorithm, so you can try this with lots of different curves. +

+

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

+ + + Скрипти вимкнено + + + + + + +

+ Finding self-intersections is effectively the same procedure, except that we're starting with a single curve, so we need to turn that into + two separate curves first. This is trivially achieved by splitting at an inflection point, or if there are none, just splitting at + t=0.5 first, and then running the exact same algorithm as above, with all non-overlapping curve pairs getting removed at each + iteration, and each successive step homing in on the curve's self-intersection points. +

+
+
+

+ + The projection identity +

+

+ De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to + draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. + Particularly this last thing is really useful because it lets us "mold" a curve, by picking it up at some point, and dragging that point + around to change the curve's shape. +

+

How does that work? Succinctly: we run de Casteljau's algorithm in reverse!

+

+ In order to run de Casteljau's algorithm in reverse, we need a few basic things: a start and end point, a point on the curve that want to + be moving around, which has an associated t value, and a point we've not explicitly talked about before, and as far as I know has + no explicit name, but lives one iteration higher in the de Casteljau process then our on-curve point does. I like to call it "A" for + reasons that will become obvious. +

+

+ So let's use graphics instead of text to see where this "A" is, because text only gets us so far: move the sliders for the following + graphics to see what, given specific t value, our A coordinate is. As well as some other coordinates, which + taken together let us derive a value that the graphics call "ratio": if you move the curve's points around, A, B, and C will move, what + happens to that value? +

+
+ + + Скрипти вимкнено + + + + + + + + Скрипти вимкнено + + + + + +
+ +

So these graphics show us several things:

+
    +
  1. a point at the tip of the curve construction's "hat": let's call that A, as well as
  2. +
  3. our on-curve point give our chosen t value: let's call that B, and finally,
  4. +
  5. + a point that we get by projecting A, through B, onto the line between the curve's start and end points: let's call that C. +
  6. +
  7. + for both quadratic and cubic curves, two points e1 and e2, which represent the single-to-last step in de + Casteljau's algorithm: in the last step, we find B at (1-t) * e1 + t * e2. +
  8. +
  9. + for cubic curves, also the points v1 and v2, which together with A represent the first step in de + Casteljau's algorithm: in the next step, we find e1 and e2. +
  10. +
+

+ These three values A, B, and C allow us to derive an important identity formula for quadratic and cubic Bézier curves: for any point on + the curve with some t value, the ratio of distances from A to B and B to C is fixed: if some t value sets up a C + that is 20% away from the start and 80% away from the end, then it doesn't matter where the start, end, or control points are; + for that t value, C will always lie at 20% from the start and 80% from the end point. Go ahead, pick an + on-curve point in either graphic and then move all the other points around: if you only move the control points, start and end won't move, + and so neither will C, and if you move either start or end point, C will move but its relative position will not change. +

+

+ So, how can we compute C? We start with our observation that C always lies somewhere between the start and ends + points, so logically C will have a function that interpolates between those two coordinates: +

+ + +

+ If we can figure out what the function u(t) looks like, we'll be done. Although we do need to remember that this + u(t) will have a different for depending on whether we're working with quadratic or cubic curves. + Running through the maths + (with thanks to Boris Zbarsky) shows us the following two formulae: +

+ + +

And

+ + +

+ So, if we know the start and end coordinates, and we know the t value, we know C, without having to calculate the + A or even B coordinates. In fact, we can do the same for the ratio function: as another function of + t, we technically don't need to know what A or B or C are, we can express it was a + pure function of t, too. +

+

We start by observing that, given A, B, and C, the following always holds:

+ + +

Working out the maths for this, we see the following two formulae for quadratic and cubic curves:

+ + +

And

+ + +

+ Which now leaves us with some powerful tools: given thee points (start, end, and "some point on the curve"), as well as a + t value, we can construct curves: we can compute C using the start and end points, and our + u(t) function, and once we have C, we can use our on-curve point (B) and the + ratio(t) function to find A: +

+ + +

+ With A found, finding e1 and e2 for quadratic curves is a matter of running the linear + interpolation with t between start and A to yield e1, and between A and end to yield + e2. For cubic curves, there is no single pair of points that can act as e1 and e2: as long as the + distance ratio between e1 to B and B to e2 is the Bézier ratio (1-t):t, + we can reverse engineer v1 and v2: +

+ + +

And then reverse engineer the curve's control control points:

+ + +

+ So: if we have a curve's start and end point, then for any t value we implicitly know all the ABC values, which (combined + with an educated guess on appropriate e1 and e2 coordinates for cubic curves) gives us the necessary information + to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three + points, which means we can also "mold" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the + curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next few sections. +

+
+
+

+ + Creating a curve from three points +

+

+ Given the preceding section, you might be wondering if we can use that knowledge to just "create" curves by placing some points and having + the computer do the rest, to which the answer is: that's exactly what we can now do! +

+

+ For quadratic curves, things are pretty easy. Technically, we'll need a t value in order to compute the ratio function used + in computing the ABC coordinates, but we can just as easily approximate one by treating the distance between the start and + B point, and B and end point as a ratio, using +

+ + +

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

+ + + Скрипти вимкнено + + + +

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

+

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

+ + + Скрипти вимкнено + + + +

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

+ + +

+ Where d is the total length of the line segment from e1 to e2. So how long do we make that? There + are again all kinds of approaches we can take, and a simple-but-effective one is to set the length of that segment to "one third the + length of the baseline". This forces e1 and e2 to always be the "linear curve" distance apart, which means if we + place our three points on a line, it will actually look like a line. Nice! The last thing we'll need to do is make sure to flip + the sign of d depending on which side of the baseline our B is located, so we don't up creating a funky curve + with a loop in it. To do this, we can use the atan2 function: +

+ + +

+ This angle φ will be between 0 and π if B is "above" the baseline (rotating all three points so that the start is on the left + and the end is the right), so we can use a relatively straight forward check to make sure we're using the correct sign for our value + d: +

+ + +

The result of this approach looks as follows:

+ + + Скрипти вимкнено + + + +

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

+ + + Скрипти вимкнено + + + +

That looks perfectly serviceable!

+

+ Of course, we can take this one step further: we can't just "create" curves, we also have (almost!) all the tools available to "mold" + curves, where we can reshape a curve by dragging a point on the curve around while leaving the start and end fixed, effectively molding + the shape as if it were clay or the like. We'll see the last tool we need to do that in the next section, and then we'll look at + implementing curve molding in the section after that, so read on! +

+
+
+

+ + Projecting a point onto a Bézier curve +

+

+ Before we can move on to actual curve molding, it'll be good if know how to actually be able to find "some point on the curve" that we're + trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the + curve our cursor will be closest to. So, how do we project points onto a curve? +

+

+ If the Bézier curve is of low enough order, we might be able to + work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a + numerical approach again. We'll be finding our ideal t value using a + binary search. First, we do a coarse distance-check based on + t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast: +

+ + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
+ +

+ After this runs, we know that LUT[i] is the coordinate on the curve in our LUT that is closest to the point we want + to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that LUT[i] is a + better guess than both LUT[i-1] and LUT[i+1], but there might be an even better projection somewhere else between those two + values, so that's what we're going to be testing for, using a variation of the binary search. +

+
    +
  1. + we start with our point p, and the t values t1=LUT[i-1].t and t2=LUT[i+1].t, which + span an interval v = t2-t1. +
  2. +
  3. + we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and + start/end points +
  4. +
  5. + we then check which of these five points is the closest to our original point p, and then repeat step 1 with the points + before and after the closest point we just found. +
  6. +
+

+ This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes + so small as to lead to distances that are, for all intents and purposes, the same for all points. +

+

+ So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller + than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bézier curve (which, of + course, you can reshape as you like). Also shown are the original three points that our coarse check finds. +

+ + + Скрипти вимкнено + + +
+
+

+ + Molding a curve +

+

+ Armed with knowledge of the "ABC" relation, point-on-curve projection, and guestimating reasonable looking helper values for cubic curve + construction, we can finally cover curve molding: updating a curve's shape interactively, by dragging points on the curve around. +

+

+ For quadratic curve, this is a really simple trick: we project our cursor onto the curve, which gives us a t value and + initial B coordinate. We don't even need the latter: with our t value and "wherever the cursor is" as target + B, we can compute the associated C: +

+ + +

And then the associated A:

+ + +

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

+ + + Скрипти вимкнено + + +

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

+

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

+ + + Скрипти вимкнено + + +

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

+

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

+ + + Скрипти вимкнено + + + + + +

+ The slide controls the "falloff distance" relative to where the original point on the curve is, so that as we drag our point around, it + interpolates with a bias towards "preserving t/e1/e2" closer to the original point, and bias + towards "idealized" form the further away we move our point, with anything that's further than our falloff distance simply + being the idealized curve. We don't even try to interpolate at that point. +

+

+ A more advanced way to try to smooth things out is to implement continuous molding, where we constantly update the curve as we + move around, and constantly change what our B point is, based on constantly projecting the cursor on the curve + as we're updating it - this is, you won't be surprised to learn, tricky, and beyond the scope of this section: interpolation + (with a reasonable distance) will do for now! +

+
+
+

+ + Curve fitting +

+

+ Given the previous section, one question you might have is "what if I don't want to guess t values?". After all, plenty of + graphics packages do automated curve fitting, so how can we implement that in a way that just finds us reasonable t values + all on its own? +

+

+ And really this is just a variation on the question "how do I get the curve through these X points?", so let's look at that. Specifically, + let's look at the answer: "curve fitting". This is in fact a rather rich field in geometry, applying to anything from data modelling to + path abstraction to "drawing", so there's a fair number of ways to do curve fitting, but we'll look at one of the most common approaches: + something called a least squares + polynomial regression. In this approach, we look at the number of points + we have in our data set, roughly determine what would be an appropriate order for a curve that would fit these points, and then tackle the + question "given that we want an nth order curve, what are the coordinates we can find such that our curve is "off" by the + least amount?". +

+

+ Now, there are many ways to determine how "off" points are from the curve, which is where that "least squares" term comes in. The most + common tool in the toolbox is to minimise the squared distance between each point we have, and the corresponding point on the + curve we end up "inventing". A curve with a snug fit will have zero distance between those two, and a bad fit will have non-zero distances + between every such pair. It's a workable metric. You might wonder why we'd need to square, rather than just ensure that distance is a + positive value (so that the total error is easy to compute by just summing distances) and the answer really is "because it tends to be a + little better". There's lots of literature on the web if you want to deep-dive the specific merits of least squared error metrics versus + least absolute error metrics, but those are well beyond the scope of this material. +

+

+ So let's look at what we end up with in terms of curve fitting if we start with the idea of performing least squares Bézier fitting. We're + going to follow a procedure similar to the one described by Jim Herold over on his + "Least Squares Bézier Fit" + article, and end with some nice interactive graphics for doing some curve fitting. +

+

+ Before we begin, we're going to use the curve in matrix form. In the section on matrices, I mentioned that some + things are easier if we use the matrix representation of a Bézier curve rather than its calculus form, and this is one of those things. +

+

+ As such, the first step in the process is expressing our Bézier curve as powers/coefficients/coordinate matrix T x M x C, + by expanding the Bézier functions. +

+
+

Revisiting the matrix representation

+

+ Rewriting Bézier functions to matrix form is fairly easy, if you first expand the function, and then arrange them into a multiple line + form, where each line corresponds to a power of t, and each column is for a specific coefficient. First, we expand the function: +

+ + +

And then we (trivially) rearrange the terms across multiple lines:

+ + +

+ This rearrangement has "factors of t" at each row (the first row is t⁰, i.e. "1", the second row is t¹, i.e. "t", the third row is t²) + and "coefficient" at each column (the first column is all terms involving "a", the second all terms involving "b", the third all terms + involving "c"). +

+

With that arrangement, we can easily decompose this as a matrix multiplication:

+ + +

We can do the same for the cubic curve, of course. We know the base function for cubics:

+ + +

So we write out the expansion and rearrange:

+ + +

Which we can then decompose:

+ + +

And, of course, we can do this for quartic curves too (skipping the expansion step):

+ + +

+ And so and on so on. Now, let's see how to use these T, M, and C, to do some curve + fitting. +

+
+ +

+ Let's get started: we're going to assume we picked the right order curve: for n points we're fitting an n-1th order curve, so we "start" with a vector P that represents the coordinates we already know, and for which + we want to do curve fitting: +

+ + +

+ Next, we need to figure out appropriate t values for each point in the curve, because we need something that lets us tie "the + actual coordinate" to "some point on the curve". There's a fair number of different ways to do this (and a large part of optimizing "the + perfect fit" is about picking appropriate t values), but in this case let's look at two "obvious" choices: +

+
    +
  1. equally spaced t values, and
  2. +
  3. t values that align with distance along the polygon.
  4. +
+

+ The first one is really simple: if we have n points, then we'll just assign each point i a t value + of (i-1)/(n-1). So if we have four points, the first point will have t=(1-1)/(4-1)=0/3, the second point will + have t=(2-1)/(4-1)=1/3, the third point will have t=2/3, and the last point will be t=1. We're just + straight up spacing the t values to match the number of points we have. +

+

+ The second one is a little more interesting: since we're doing polynomial regression, we might as well exploit the fact that our base + coordinates just constitute a collection of line segments. At the first point, we're fixing t=0, and the last point, we want t=1, and + anywhere in between we're simply going to say that t is equal to the distance along the polygon, scaled to the [0,1] domain. +

+

To get these values, we first compute the general "distance along the polygon" matrix:

+ + +

+ Where length() is literally just that: the length of the line segment between the point we're looking at, and the previous + point. This isn't quite enough, of course: we still need to make sure that all the values between i=1 and + i=n fall in the [0,1] interval, so we need to scale all values down by whatever the total length of the polygon is: +

+ + +

+ And now we can move on to the actual "curve fitting" part: what we want is a function that lets us compute "ideal" control point values + such that if we build a Bézier curve with them, that curve passes through all our original points. Or, failing that, have an overall error + distance that is as close to zero as we can get it. So, let's write out what the error distance looks like. +

+

+ As mentioned before, this function is really just "the distance between the actual coordinate, and the coordinate that the curve evaluates + to for the associated t value", which we'll square to get rid of any pesky negative signs: +

+ + +

+ Since this function only deals with individual coordinates, we'll need to sum over all coordinates in order to get the full error + function. So, we literally just do that; the total error function is simply the sum of all these individual errors: +

+ + +

+ And here's the trick that justifies using matrices: while we can work with individual values using calculus, with matrices we can compute + as many values as we make our matrices big, all at the "same time", We can replace the individual terms pi with the full + P coordinate matrix, and we can replace Bézier(si) with the matrix representation + T x M x C we talked about before, which gives us: +

+ + +

In which we can replace the rather cumbersome "squaring" operation with a more conventional matrix equivalent:

+ + +

+ Here, the letter T is used instead of the number 2, to represent the + matrix transpose; each row in the original matrix becomes a column in the transposed + matrix instead (row one becomes column one, row two becomes column two, and so on). +

+

+ This leaves one problem: T isn't actually the matrix we want: we don't want symbolic t values, we want the + actual numerical values that we computed for S, so we need to form a new matrix, which we'll call 𝕋, that makes use of + those, and then use that 𝕋 instead of T in our error function: +

+ + +

Which, because of the first and last values in S, means:

+ + +

Now we can properly write out the error function as matrix operations:

+ + +

+ So, we have our error function: we now need to figure out the expression for where that function has minimal value, e.g. where the error + between the true coordinates and the coordinates generated by the curve fitting is smallest. Like in standard calculus, this requires + taking the derivative, and determining where that derivative is zero: +

+ + +
+

Where did this derivative come from?

+

+ That... is a good question. In fact, when trying to run through this approach, I ran into the same question! And you know what? I + straight up had no idea. I'm decent enough at calculus, I'm decent enough at linear algebra, and I just don't know. +

+

+ So I did what I always do when I don't understand something: I asked someone to help me understand how things work. In this specific + case, I + posted a question + to Math.stackexchange, and received a answer that goes into way more detail than I had + hoped to receive. +

+

+ Is that answer useful to you? Probably: no. At least, not unless you like understanding maths on a recreational level. And I do mean + maths in general, not just basic algebra. But it does help in giving us a reference in case you ever wonder "Hang on. Why was that + true?". There are answers. They might just require some time to come to understand. +

+
+ +

+ Now, given the above derivative, we can rearrange the terms (following the rules of matrix algebra) so that we end up with an expression + for C: +

+ + +

+ Here, the "to the power negative one" is the notation for the + matrix inverse. But that's all we have to do: we're done. Starting with + P and inventing some t values based on the polygon the coordinates in P define, we can + compute the corresponding Bézier coordinates C that specify a curve that goes through our points. Or, if it can't go + through them exactly, as near as possible. +

+

+ So before we try that out, how much code is involved in implementing this? Honestly, that answer depends on how much you're going to be + writing yourself. If you already have a matrix maths library available, then really not that much code at all. On the other hand, if you + are writing this from scratch, you're going to have to write some utility functions for doing your matrix work for you, so it's really + anywhere from 50 lines of code to maybe 200 lines of code. Not a bad price to pay for being able to fit curves to pre-specified + coordinates. +

+

+ So let's try it out! The following graphic lets you place points, and will start computing exact-fit curves once you've placed at least + three. You can click for more points, and the code will simply try to compute an exact fit using a Bézier curve of the appropriate order. + Four points? Cubic Bézier. Five points? Quartic. And so on. Of course, this does break down at some point: depending on where you place + your points, it might become mighty hard for the fitter to find an exact fit, and things might actually start looking horribly off once + there's enough points for compound + floating point rounding errors to start making a + difference (which is around 10~11 points). +

+ + + Скрипти вимкнено + + + + + + +

+ You'll note there is a convenient "toggle" buttons that lets you toggle between equidistant t values, and distance ratio + along the polygon formed by the points. Arguably more interesting is that once you have points to abstract a curve, you also get + direct control over the time values through sliders for each, because if the time values are our degree of freedom, you should be + able to freely manipulate them and see what the effect on your curve is. +

+
+
+

+ + Bézier curves and Catmull-Rom curves +

+

+ Taking an excursion to different splines, the other common design curve is the + Catmull-Rom spline, which unlike Bézier curves + pass through each control point, so they offer a kind of "built-in" curve fitting. +

+

+ In fact, let's start with just playing with one: the following graphic has a predefined curve that you manipulate the points for, and lets + you add points by clicking/tapping the background, as well as let you control "how fast" the curve passes through its point using the + tension slider. The tenser the curve, the more the curve tends towards straight lines from one point to the next. +

+ + + Скрипти вимкнено + + + + + +

+ Now, it may look like Catmull-Rom curves are very different from Bézier curves, because these curves can get very long indeed, but what + looks like a single Catmull-Rom curve is actually a spline: a single + curve built up of lots of identically-computed pieces, similar to if you just took a whole bunch of Bézier curves, placed them end to end, + and lined up their control points so that things look like a single curve. For a Catmull-Rom curve, each "piece" between two points is + defined by the point's coordinates, and the tangent for those points, the latter of which + can trivially be derived from knowing the + previous and next point: +

+ + +

+ One downside of this is that—as you may have noticed from the graphic—the first and last point of the overall curve don't actually join up + with the rest of the curve: they don't have a previous/next point respectively, and so there is no way to calculate what their tangent + should be. Which also makes it rather tricky to fit a Catmull-Rom curve to three points like we were able to do for Bézier curves. More on + that in the next section. +

+

+ In fact, before we move on, let's look at how to actually draw the basic form of these curves (I say basic, because there are a number of + variations that make things + considerable more + complex): +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ +

+ Now, since a Catmull-Rom curve is a form of cubic Hermite spline, and as + cubic Bézier curves are also a form of cubic Hermite spline, we run into an interesting bit of maths programming: we can convert + one to the other and back, and the maths for doing so is surprisingly simple! +

+

The main difference between Catmull-Rom curves and Bézier curves is "what the points mean":

+
    +
  • + A cubic Bézier curve is defined by a start point, a control point that implies the tangent at the start, a control point that implies + the tangent at the end, and an end point, plus a characterizing matrix that we can multiply by that point vector to get on-curve + coordinates. +
  • +
  • + A Catmull-Rom curve is defined by a start point, a tangent that for that starting point, an end point, and a tangent for that end point, + plus a characteristic matrix that we can multiple by the point vector to get on-curve coordinates. +
  • +
+

+ Those are very similar, so let's see exactly how similar they are. We've already see the matrix form for Bézier curves, + so how different is the matrix form for Catmull-Rom curves?: +

+ + +

+ That's pretty dang similar. So the question is: how can we convert that expression with Catmull-Rom matrix and vector into an expression + of the Bézier matrix and vector? The short answer is of course "by using linear algebra", but the longer answer is the rest of this + section, and involves some maths that you may not even care for: if you just want to know the (incredibly simple) conversions between the + two curve forms, feel free to skip to the end of the following explanation, but if you want to how we can get one from the + other... let's get mathing! +

+
+

Deriving the conversion formulae

+

+ In order to convert between Catmull-Rom curves and Bézier curves, we need to know two things. Firstly, how to express the Catmull-Rom + curve using a "set of four coordinates", rather than a mix of coordinates and tangents, and secondly, how to convert those Catmull-Rom + coordinates to and from Bézier form. +

+

+ We start with the first part, to figure out how we can go from Catmull-Rom V coordinates to Bézier + P coordinates, by applying "some matrix T". We don't know what that T is yet, but + we'll get to that: +

+ + +

+ So, this mapping says that in order to map a Catmull-Rom "point + tangent" vector to something based on an "all coordinates" vector, we + need to determine the mapping matrix such that applying T yields P2 as start point, P3 as end point, and two tangents based on + the lines between P1 and P3, and P2 nd P4, respectively. +

+

Computing T is really more "arranging the numbers":

+ + +

Thus:

+ + +

+ However, we're not quite done, because Catmull-Rom curves have that "tension" parameter, written as τ (a lowercase"tau"), which + is a scaling factor for the tangent vectors: the bigger the tension, the smaller the tangents, and the smaller the tension, the bigger + the tangents. As such, the tension factor goes in the denominator for the tangents, and before we continue, let's add that tension + factor into both our coordinate vector representation, and mapping matrix T: +

+ + +

+ With the mapping matrix properly done, let's rewrite the "point + tangent" Catmull-Rom matrix form to a matrix form in terms of four + coordinates, and see what we end up with: +

+ + +

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

+ + +

and merge the matrices:

+ + +

This looks a lot like the Bézier matrix form, which as we saw in the chapter on Bézier curves, should look like this:

+ + +

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

+ + +

Into something that looks like this:

+ + +

And the way we do that is with a fairly straight forward bit of matrix rewriting. We start with the equality we need to ensure:

+ + +

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

+ + +

Then we can "get rid of" the Bézier matrix on the right by left-multiply both with the inverse of the Bézier matrix:

+ + +

+ A matrix times its inverse is the matrix equivalent of 1, and because "something times 1" is the same as "something", so we can just + outright remove any matrix/inverse pair: +

+ + +

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

+ + +

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

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

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

+
+ +

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

+ + +

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

+ + +

Or, if your API allows you to specify Catmull-Rom curves using plain coordinates:

+ + +
+
+

+ + Creating a Catmull-Rom curve from three points +

+

+ Much shorter than the previous section: we saw that Catmull-Rom curves need at least 4 points to draw anything sensible, so how do we + create a Catmull-Rom curve from three points? +

+

Short and sweet: we don't.

+

+ We run through the maths that lets us create a cubic Bézier curve, and then convert its coordinates to + Catmull-Rom form using the conversion formulae we saw above. +

+
+
+

+ + Forming poly-Bézier curves +

+

+ Much like lines can be chained together to form polygons, Bézier curves can be chained together to form poly-Béziers, and the only trick + required is to make sure that: +

+
    +
  1. the end point of each section is the starting point of the following section, and
  2. +
  3. the derivatives across that dual point line up.
  4. +
+

Unless you want sharp corners, of course. Then you don't even need 2.

+

+ We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind that just follows point 1. where the end + point of a segment is the same point as the start point of the next segment. This leads to poly-Béziers that are pretty hard to work with, + but they're the easiest to implement: +

+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +

+ Dragging the control points around only affects the curve segments that the control point belongs to, and moving an on-curve point leaves + the control points where they are, which is not the most useful for practical modelling purposes. So, let's add in the logic we need to + make things a little better. We'll start by linking up control points by ensuring that the "incoming" derivative at an on-curve point is + the same as it's "outgoing" derivative: +

+ + +

+ We can effect this quite easily, because we know that the vector from a curve's last control point to its last on-curve point is equal to + the derivative vector. If we want to ensure that the first control point of the next curve matches that, all we have to do is mirror that + last control point through the last on-curve point. And mirroring any point A through any point B is really simple: +

+ + +

+ So let's implement that and see what it gets us. The following two graphics show a quadratic and a cubic poly-Bézier curve again, but this + time moving the control points around moves others, too. However, you might see something unexpected going on for quadratic curves... +

+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +

+ As you can see, quadratic curves are particularly ill-suited for poly-Bézier curves, as all the control points are effectively linked. + Move one of them, and you move all of them. Not only that, but if we move the on-curve points, it's possible to get a situation where a + control point cannot satisfy the constraint that it's the reflection of its two neighbouring control points... This means that we cannot + use quadratic poly-Béziers for anything other than really, really simple shapes. And even then, they're probably the wrong choice. Cubic + curves are pretty decent, but the fact that the derivatives are linked means we can't manipulate curves as well as we might if we relaxed + the constraints a little. +

+

So: let's relax the requirement a little.

+

+ We can change the constraint so that we still preserve the angle of the derivatives across sections (so transitions from one + section to the next will still look natural), but give up the requirement that they should also have the same vector length. + Doing so will give us a much more useful kind of poly-Bézier curve: +

+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +

+ Cubic curves are now better behaved when it comes to dragging control points around, but the quadratic poly-Bézier still has the problem + that moving one control points will move the control points and may ending up defining "the next" control point in a way that doesn't + work. Quadratic curves really aren't very useful to work with... +

+

+ Finally, we also want to make sure that moving the on-curve coordinates preserves the relative positions of the associated control points. + With that, we get to the kind of curve control that you might be familiar with from applications like Photoshop, Inkscape, Blender, etc. +

+ + + Скрипти вимкнено + + + + + + Скрипти вимкнено + + + +

+ Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an + on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, + for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that + good... +

+

+ A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled + segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the + reader. +

+
+
+

+ + Curve offsetting +

+

+ Perhaps you're like me, and you've been writing various small programs that use Bézier curves in some way or another, and at some point + you make the step to implementing path extrusion. But you don't want to do it pixel based; you want to stay in the vector world. You find + that extruding lines is relatively easy, and tracing outlines is coming along nicely (although junction caps and fillets are a bit of a + hassle), and then you decide to do things properly and add Bézier curves to the mix. Now you have a problem. +

+

+ Unlike lines, you can't simply extrude a Bézier curve by taking a copy and moving it around, because of the curvatures; rather than a + uniform thickness, you get an extrusion that looks too thin in places, if you're lucky, but more likely will self-intersect. The trick, + then, is to scale the curve, rather than simply copying it. But how do you scale a Bézier curve? +

+

+ Bottom line: you can't. So you cheat. We're not going to do true curve scaling, or rather curve offsetting, because + that's impossible. Instead we're going to try to generate 'looks good enough' offset curves. +

+
+

"What do you mean, you can't? Prove it."

+

+ First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using + a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it + cannot be represented as a Bézier curve, which has to be a polynomial. Let's look at why this is: +

+

+ From a mathematical point of view, an offset curve O(t) is a curve such that, given our original curve B(t), + any point on O(t) is a fixed distance d away from coordinate B(t). So let's math that: +

+ + +

+ However, we're working in 2D, and d is a single value, so we want to turn it into a vector. If we want a point distance + d "away" from the curve B(t) then what we really mean is that we want a point at d times the + "normal vector" from point B(t), where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent + at B(t). Easy enough: +

+ + +

+ Now this still isn't very useful unless we know what the formula for N(t) is, so let's find out. N(t) runs + perpendicular to the original curve tangent, and we know that the tangent is simply B'(t), so we could just rotate that 90 + degrees and be done with it. However, we need to ensure that N(t) has the same magnitude for every t, or the + offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure + N(t) always has length 1, which we can achieve by dividing B'(t) by its magnitude: +

+ + +

+ Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc + length from some start a to end b, we must use the formula we saw earlier. Noting that "length" is usually + denoted with double vertical bars: +

+ + +

+ So if we want the length of the tangent, we plug in B'(t), with t = 0 as start and t = 1 as end: +

+ + +

+ And that's where things go wrong. It doesn't even really matter what the second derivative for B(t) is, that square root is + screwing everything up, because it turns our nice polynomials into things that are no longer polynomials. +

+

+ There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with + unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine + because they do, but they don't; remember that only the base function has binomial coefficients. That's before we + factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make + all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call + them circles, and they have much simpler functions than Bézier curves. +

+

+ So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means + N(t) won't be a polynomial, which means that d times N(t) won't be a polynomial, which means + that, ultimately, O(t) won't be a polynomial, which means that even if we can determine the function for + O(t) just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve. +

+

+ And that's one reason why Bézier curves are tricky: there are actually a lot of curves that cannot be represented as a Bézier + curve at all. They can't even model their own offset curves. They're weird that way. So how do all those other programs do it? Well, + much like we're about to do, they cheat. We're going to approximate an offset curve in a way that will look relatively close to what the + real offset curve would look like, if we could compute it. +

+
+ +

+ So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. + However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the + baseline, and the midpoint of the curve at t=0.5 is roughly in the center of the polygon defined by the curve coordinates) + and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and + end points). +

+

+ A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and + use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based + on where the center of the curve is. If the on-curve point for t=0.5 is too far off from the center, we simply split the + segment down the middle. Generally this is more than enough to end up with safe segments. +

+

+ The following graphics show off curve offsetting, and you can use the slider to control the distance at which the curve gets offset. The + curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, + particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs + to be reduced in order to get segments that can safely be scaled. +

+ + + Скрипти вимкнено + + + + + + + + Скрипти вимкнено + + + + + +

+ You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that + we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve + is large enough, this may still lead to incorrect offsets. +

+
+
+

+ + Graduated curve offsetting +

+

+ What if we want to do graduated offsetting, starting at some distance s but ending at some other distance e? + Well, if we can compute the length of a curve (which we can if we use the Legendre-Gauss quadrature approach) then we can also determine + how far "along the line" any point on the curve is. With that knowledge, we can offset a curve so that its offset curve is not uniformly + wide, but graduated between with two different offset widths at the start and end. +

+

+ Like normal offsetting we cut up our curve in sub-curves, and then check at which distance along the original curve each sub-curve starts + and ends, as well as to which point on the curve each of the control points map. This gives us the distance-along-the-curve for each + interesting point in the sub-curve. If we call the total length of all sub-curves seen prior to seeing "the current" sub-curve + S (and if the current sub-curve is the first one, S is zero), and we call the full length of our original curve + L, then we get the following graduation values: +

+
    +
  • start: map S from interval (0,L) to interval (s,e)
  • +
  • c1: map(<strong>S+d1</strong>, 0,L, s,e), d1 = distance along curve to projection of c1
  • +
  • c2: map(<strong>S+d2</strong>, 0,L, s,e), d2 = distance along curve to projection of c2
  • +
  • ...
  • +
  • end: map(<strong>S+length(subcurve)</strong>, 0,L, s,e)
  • +
+

+ At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so + offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. + Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled + with your up and down arrow keys): +

+ + + Скрипти вимкнено + + + + + + + + Скрипти вимкнено + + + + + +
+
+

+ + Circles and quadratic Bézier curves +

+

+ Circles and Bézier curves are very different beasts, and circles are infinitely easier to work with than Bézier curves. Their formula is + much simpler, and they can be drawn more efficiently. But, sometimes you don't have the luxury of using circles, or ellipses, or arcs. + Sometimes, all you have are Bézier curves. For instance, if you're doing font design, fonts have no concept of geometric shapes, they only + know straight lines, and Bézier curves. OpenType fonts with TrueType outlines only know quadratic Bézier curves, and OpenType fonts with + Type 2 outlines only know cubic Bézier curves. So how do you draw a circle, or an ellipse, or an arc? +

+

You approximate.

+

+ We already know that Bézier curves cannot model all curves that we can think of, and this includes perfect circles, as well as ellipses, + and their arc counterparts. However, we can certainly approximate them to a degree that is visually acceptable. Quadratic and cubic curves + offer us different curvature control, so in order to approximate a circle we will first need to figure out what the error is if we try to + approximate arcs of increasing degree with quadratic and cubic curves, and where the coordinates even lie. +

+

+ Since arcs are mid-point-symmetrical, we need the control points to set up a symmetrical curve. For quadratic curves this means that the + control point will be somewhere on a line that intersects the baseline at a right angle. And we don't get any choice on where that will + be, since the derivatives at the start and end point have to line up, so our control point will lie at the intersection of the tangents at + the start and end point. +

+

+ First, let's try to fit the quadratic curve onto a circular arc. In the following sketch you can move the mouse around over a unit circle, + to see how well, or poorly, a quadratic curve can approximate the arc from (1,0) to where your mouse cursor is: +

+ + + Скрипти вимкнено + + + + + +

+ As you can see, things go horribly wrong quite quickly; even trying to approximate a quarter circle using a quadratic curve is a bad idea. + An eighth of a turns might look okay, but how okay is okay? Let's apply some maths and find out. What we're interested in is how far off + our on-curve coordinates are with respect to a circular arc, given a specific start and end angle. We'll be looking at how much space + there is between the circular arc, and the quadratic curve's midpoint. +

+

+ We start out with our start and end point, and for convenience we will place them on a unit circle (a circle around 0,0 with radius 1), at + some angle φ: +

+ + +

What we want to find is the intersection of the tangents, so we want a point C such that:

+ + +

+ i.e. we want a point that lies on the vertical line through S (at some distance a from S) and also lies on the tangent line + through E (at some distance b from E). Solving this gives us: +

+ + +

First we solve for b:

+ + +

which yields:

+ + +

which we can then substitute in the expression for a:

+ + +

+ A quick check shows that plugging these values for a and b into the expressions for Cx and Cy give + the same x/y coordinates for both "a away from A" and "b away from B", so let's continue: now that we know the + coordinate values for C, we know where our on-curve point T for t=0.5 (or angle φ/2) is, because we can just evaluate the Bézier + polynomial, and we know where the circle arc's actual point P is for angle φ/2: +

+ + +

We compute T, observing that if t=0.5, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:

+ + +

Which, worked out for the x and y components, gives:

+ + +

And the distance between these two is the standard Euclidean distance:

+ + +

+ So, what does this distance function look like when we plot it for a number of ranges for the angle φ, such as a half circle, quarter + circle and eighth circle? +

+ + + + + + + + +
+ + plotted for 0 ≤ φ ≤ π: + + + plotted for 0 ≤ φ ≤ ½π: + + + + + plotted for 0 ≤ φ ≤ ¼π: +
+ +

+ We now see why the eighth circle arc looks decent, but the quarter circle arc doesn't: an error of roughly 0.06 at t=0.5 means + we're 6% off the mark... we will already be off by one pixel on a circle with pixel radius 17. Any decent sized quarter circle arc, say + with radius 100px, will be way off if approximated by a quadratic curve! For the eighth circle arc, however, the error is only roughly + 0.003, or 0.3%, which explains why it looks so close to the actual eighth circle arc. In fact, if we want a truly tiny error, like 0.001, + we'll have to contend with an angle of (rounded) 0.593667, which equates to roughly 34 degrees. We'd need 11 quadratic curves to form a + full circle with that precision! (technically, 10 and ten seventeenth, but we can't do partial curves, so we have to round up). That's a + whole lot of curves just to get a shape that can be drawn using a simple function! +

+

+ In fact, let's flip the function around, so that if we plug in the precision error, labelled ε, we get back the maximum angle for that + precision: +

+ + +

+ And frankly, things are starting to look a bit ridiculous at this point, we're doing way more maths than we've ever done, but thankfully + this is as far as we need the maths to take us: If we plug in the precisions 0.1, 0.01, 0.001 and 0.0001 we get the radians values 1.748, + 1.038, 0.594 and 0.3356; in degrees, that means we can cover roughly 100 degrees (requiring four curves), 59.5 degrees (requiring six + curves), 34 degrees (requiring 11 curves), and 19.2 degrees (requiring a whopping nineteen curves). +

+

+ The bottom line? Quadratic curves are kind of lousy if you want circular (or elliptical, which are circles that have been + squashed in one dimension) curves. We can do better, even if it's just by raising the order of our curve once. So let's try the same thing + for cubic curves. +

+
+
+

+ + Circles and cubic Bézier curves +

+

+ In the previous section we tried to approximate a circular arc with a quadratic curve, and it mostly made us unhappy. Cubic curves are + much better suited to this task, so what do we need to do? +

+

+ For cubic curves, we basically want the curve to pass through three points on the circle: the start point, the mid point at "angle/2", and + the end point at "angle". We then also need to make sure the control points are such that the start and end tangent lines line up with the + circle's tangent lines at the start and end point. +

+

+ The first thing we can do is "guess" what the curve should look like, based on the previously outlined curve-through-three-points + procedure. This will give use a curve with correct start, mid and end points, but possibly incorrect derivatives at the start and end, + because the control points might not be in the right spot. We can then slide the control points along the lines that connect them to their + respective end point, until they effect the corrected derivative at the start and end points. However, if you look back at the section on + fitting curves through three points, the rules used were such that they optimized for a near perfect hemisphere, so using the same guess + won't be all that useful: guessing the solution based on knowing the solution is not really guessing. +

+

+ So have a graphical look at a "bad" guess versus the true fit, where we'll be using the bad guess and the description in the second + paragraph to derive the maths for the true fit: +

+ + + Скрипти вимкнено + + + + + +

+ We see two curves here; in blue, our "guessed" curve and its control points, and in grey/black, the true curve fit, with proper control + points that were shifted in, along line between our guessed control points, such that the derivatives at the start and end points are + correct. +

+

+ We can already see that cubic curves are a lot better than quadratic curves, and don't look all that wrong until we go well past a quarter + circle; ⅜th starts to hint at problems, and half a circle has an obvious "gap" between the real circle and the cubic approximation. + Anything past that just looks plain ridiculous... but quarter curves actually look pretty okay! +

+

So, maths time again: how okay is "okay"? Let's apply some more maths to find out.

+

+ Unlike for the quadratic curve, we can't use t=0.5 as our reference point because by its very nature it's one of the three points + that are actually guaranteed to lie on the circular curve. Instead, we need a different t value. If we run some analysis on the + curve we find that the actual t value at which the curve is furthest from what it should be is 0.211325 (rounded), but we don't + know "why", since finding this value involves root-finding, and is nearly impossible to do symbolically without pages and pages of math + just to express one of the possible solutions. +

+

+ So instead of walking you through the derivation for that value, let's simply take that t value and see what the error is for + circular arcs with an angle ranging from 0 to 2π: +

+ + + + + + + + +
+ + plotted for 0 ≤ φ ≤ 2π: + + + plotted for 0 ≤ φ ≤ π: + + + plotted for 0 ≤ φ ≤ ½π: +
+ +

+ We see that cubic Bézier curves are much better when it comes to approximating circular arcs, with an error of less than 0.027 at the two + "bulge" points for a quarter circle (which had an error of 0.06 for quadratic curves at the mid point), and an error near 0.001 for an + eighth of a circle, so we're getting less than half the error for a quarter circle, or: at a slightly lower error, we're getting twice the + arc. This makes cubic curves quite useful! +

+

+ In fact, the precision of a cubic curve at a quarter circle is considered "good enough" by so many people that it's generally considered + "just fine" to use four cubic Bézier curves to fake a full circle when no circle primitives are available; generally, people won't notice + that it's not a real circle unless you also happen to overlay an actual circle, so that the difference becomes obvious. +

+

+ So with the error analysis out of the way, how do we actually compute the coordinates needed to get that "true fit" cubic curve? The first + observation is that we already know the start and end points, because they're the same as for the quadratic attempt: +

+ + +

+ But we now need to find two control points, rather than one. If we want the derivatives at the start and end point to match the circle, + then the first control point can only lie somewhere on the vertical line through S, and the second control point can only lie somewhere on + the line tangent to point E, which means: +

+ + +

where "a" is some scaling factor we'll need to find the expression for, and:

+ + +

using the same scaling factor, because circular arcs are symmetrical, so our approximation will need to be symmetrical, too.

+

+ Starting with this information, we slowly maths our way to success, but I won't lie: the maths for this is pretty trig-heavy, and it's + easy to get lost if you remember (or know!) some of the core trigonometric identities, so if you just want to see the final result just + skip past the next section! +

+
+

Let's do this thing.

+

+ Unlike for the quadratic case, we need some more information in order to compute a and b, since they're no longer + dependent variables. First, we observe that the curve is symmetrical, so whatever values we end up finding for C1 will apply + to C2 as well (rotated along its tangent), so we'll focus on finding the location of C1 only. So here's where we + do something that you might not expect: we're going to ignore for a moment, because we're going to have a much easier time if we just + solve this problem with geometry first, then move to calculus to solve a much simpler problem. +

+

+ If we look at the triangle that is formed between our starting point, or initial guess C1 and our real C1, there's + something funny going on: if we treat the line {start,guess} as our opposite side, the line {guess,real} as our adjacent side, with + {start,real} our hypotenuse, then the angle for the corner hypotenuse/adjacent is half that of the arc we're covering. Try it: if you + place the end point at a quarter circle (pi/2, or 90 degrees), the angle in our triangle is half a quarter (pi/4, or 45 degrees). With + that knowledge, and a knowledge of what the length of any of our lines segments are (as a function), we can determine where our control + points are, and thus have everything we need to find the error distance function. Of the three lines, the one we can easiest determine + is {start,guess}, so let's find out what the guessed control point is. Again geometrically, because we have the benefit of an on-curve + t=0.5 value. +

+

+ The distance from our guessed point to the start point is exactly the same as the projection distance we looked at earlier. Using + t=0.5 as our point "B" in the "A,B,C" projection, then we know the length of the line segment {C,A}, since it's d1 = + {A,B} + d2 = {B,C}: +

+ + +

+ So that just leaves us to find the distance from t=0.5 to the baseline for an arbitrary angle φ, which is the distance from the + centre of the circle to our t=0.5 point, minus the distance from the centre to the line that runs from start point to end point. + The first is the same as the point P we found for the quadratic curve: +

+ + +

+ And the distance from the origin to the line start/end is another application of angles, since the triangle {origin,start,C} has known + angles, and two known sides. We can find the length of the line {origin,C}, which lets us trivially compute the coordinate for C: +

+ + +

+ With the coordinate C, and knowledge of coordinate B, we can determine coordinate A, and get a vector that is identical to the vector + {start,guess}: +

+ + + + +

+ Which means we can now determine the distance {start,guessed}, which is the same as the distance {C,A}, and use that to determine the + vertical distance from our start point to our C1: +

+ + +

+ And after this tedious detour to find the coordinate for C1, we can find C2 fairly simply, since it's lies at + distance -C1y along the end point's tangent: +

+ + +

And that's it, we have all four points now for an approximation of an arbitrary circular arc with angle φ.

+
+ +

So, to recap, given an angle φ, the new control coordinates are:

+ + +

and

+ + +

+ And, because the "quarter curve" special case comes up so incredibly often, let's look at what these new control points mean for the curve + coordinates of a quarter curve, by simply filling in φ = π/2: +

+ + +

Which, in decimal values, rounded to six significant digits, is:

+ + +

+ Of course, this is for a circle with radius 1, so if you have a different radius circle, simply multiply the coordinate by the radius you + need. And then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin: +

+ + + Скрипти вимкнено + + +
+
+

+ + Approximating Bézier curves with circular arcs +

+

+ Let's look at doing the exact opposite of the previous section: rather than approximating circular arc using Bézier curves, let's + approximate Bézier curves using circular arcs. +

+

+ We already saw in the section on circle approximation that this will never yield a perfect equivalent, but sometimes you need circular + arcs, such as when you're working with fabrication machinery, or simple vector languages that understand lines and circles, but not much + else. +

+

+ The approach is fairly simple: pick a starting point on the curve, and pick two points that are further along the curve. Determine the + circle that goes through those three points, and see if it fits the part of the curve we're trying to approximate. Decent fit? Try spacing + the points further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've found the "good approximation/bad + approximation" boundary, record the "good" arc, and then move the starting point up to overlap the end point we previously found. Rinse + and repeat until we've covered the entire curve. +

+

+ We already saw how to fit a circle through three points in the section on creating a curve from three points, + and finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, + and the arc has to necessarily go from the start point, to the end point, over the remaining point. +

+

So, how can we convert a Bézier curve into a (sequence of) circular arc(s)?

+
    +
  • Start at t=0
  • +
  • Pick two points further down the curve at some value m = t + n and e = t + 2n
  • +
  • Find the arc that these points define
  • +
  • + Determine how close the found arc is to the curve: +
      +
    • Pick two additional points e1 = t + n/2 and e2 = t + n + n/2.
    • +
    • + These points, if the arc is a good approximation of the curve interval chosen, should lie on the circle, so their + distance to the center of the circle should be the same as the distance from any of the three other points to the center. +
    • +
    • + For point points, determine the (absolute) error between the radius of the circle, and the actual distance from the + center of the circle to the point on the curve. +
    • +
    • If this error is too high, we consider the arc bad, and try a smaller interval.
    • +
    +
  • +
+

+ The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's the entire curve. The midpoint is + simply at t=0.5, and then we start performing a + binary search. +

+
    +
  1. We start with low=0, mid=0.5 and high=1
  2. +
  3. + That'll fail, so we retry with the interval halved: {0, 0.25, 0.5} +
      +
    • If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
    • +
    • However, if the arc was still bad, we move down by half the distance: {0, 0.125, 0.25}.
    • +
    +
  4. +
  5. + We keep doing this over and over until we have two arcs, in sequence, of which the first arc is good, and the second arc is bad. When we + find that pair, we've found the boundary between a good approximation and a bad approximation, and we pick the good arc. +
  6. +
+

+ The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a + combined half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but + already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or + decrease the error threshold, to see what the effect of a smaller or larger error threshold is. +

+ + + Скрипти вимкнено + + + + + +

+ With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined + arc's starting point, and using points further down the curve. We keep trying this until the found end point is for t=1, at which + point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you + can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve: +

+ + + Скрипти вимкнено + + + + + +

+ So... what is this good for? Obviously, if you're working with technologies that can't do curves, but can do lines and circles, then the + answer is pretty straightforward, but what else? There are some reasons why you might need this technique: using circular arcs means you + can determine whether a coordinate lies "on" your curve really easily (simply compute the distance to each circular arc center, and if any + of those are close to the arc radii, at an angle between the arc start and end, bingo, this point can be treated as lying "on the curve"). + Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also + trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this + is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how + much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which, + based on your application! +

+
+
+

+ + B-Splines +

+

+ No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily + confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference, + and we'll be looking at those differences in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves + (that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them, + and how to draw them based on a number of parameters that you can pick for individual B-Splines. +

+

+ First off: B-Splines are piecewise, + polynomial interpolation curves, where the "single curve" is built by + performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" + B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can + be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly + connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections. +

+

+ What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the + spline curve drawn. +

+ + + Скрипти вимкнено + + +

+ The important part to notice here is that we are not doing the same thing with B-Splines that we do for poly-Béziers or + Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic + poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that + follows, so we can only have 4, 7, 10, 13, 16, etc. point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single + points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth + interpolations of each possible curve involving four consecutive points, such that at any point along the curve except for our + start and end points, our on-curve coordinate is defined by four control points. +

+

Consider the difference to be this:

+
    +
  • for Bézier curves, the curve is defined as an interpolation of points, but:
  • +
  • for B-Splines, the curve is defined as an interpolation of curves.
  • +
+

In fact, let's look at that again, but this time with the base curves shown, too. Each consecutive four points define one curve:

+ + + Скрипти вимкнено + + + + + +

+ In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have + a look at how things work. +

+

How to compute a B-Spline curve: some maths

+

+ Given a B-Spline of degree d and thus order k=d+1 (so a quadratic B-Spline is degree 2 and order 3, a cubic + B-Spline is degree 3 and order 4, etc) and n control points P0 through Pn-1, we can compute a point on the curve for some value t in the interval [0,1] (where 0 is the start of the curve, and 1 the + end, just like for Bézier curves), by evaluating the following function: +

+ + +

+ Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control + points, weighted somehow", where the weighting is achieved through the N(...) function, subscripted with an obvious parameter + i, which comes from our summation, and some magical parameter k. So we need to know two things: 1. what does + N(t) do, and 2. what is that k? Let's cover both, in reverse order. +

+

+ The parameter k represents the "knot interval" over which a section of curve is defined. As we learned earlier, a B-Spline + curve is itself an interpolation of curves, and we can treat each transition where a control point starts or stops influencing the total + curvature as a "knot on the curve". Doing so for a degree d B-Spline with n control point gives us + d + n + 1 knots, defining d + n intervals along the curve, and it is these intervals that the above + k subscript to the N() function applies to. +

+

Then the N() function itself. What does it look like?

+ + +

+ So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation, on a specific + knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that this is a recursive + iteration where i goes up, and k goes down, so it seem reasonable to expect that this recursion has to stop at + some point; obviously, it does, and specifically it does so for the following i/k values: +

+ + +

+ And this function finally has a straight up evaluation: if a t value lies within a knot-specific interval once we reach a + k=1 value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale + our t value first, so that it lies in the interval bounded by knots[d] and knots[n], which are the + start point and end point where curvature is controlled by exactly order control points. For instance, for degree 3 (=order + 4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map t from [the interval 0,1] to the interval [4,8], + and then use that value in the functions above, instead. +

+

Can we simplify that?

+

We can, yes.

+

+ People far smarter than us have looked at this work, and two in particular — + Maurice Cox and + Carl de Boor — came to a mathematically pleasing solution: to compute a point + P(t), we can compute this point by evaluating d(t) on a curve section between knots i and i+1: +

+ + +

+ This is another recursive function, with k values decreasing from the curve order to 1, and the value α (alpha) defined + by: +

+ + +

+ That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers. And, once we have our alpha + value, we also have (1-alpha) because it's a trivial subtraction. Computing the d() function is thus mostly a + matter of computing pretty simple arithmetical statements, with some caching of results so we can refer to them as we recurve. While the + recursion might see computationally expensive, the total algorithm is cheap, as each step only involves very simple maths. +

+

Of course, the recursion does need a stop condition:

+ + +

+ So, we actually see two stopping conditions: either i becomes 0, in which case d() is zero, or + k becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above. +

+

+ Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily using the same kind of linear interpolation we saw in de + Casteljau's algorithm. For instance, if we write out d() for i=3 and k=3, we get the following + recursion diagram: +

+ + +

+ That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2), where those two are themselves a + mixture of d(1,3) and d(1,2), and d(1,2) and d(1,1), respectively, which are + themselves a mixture of etc. etc. We simply keep expanding our terms until we reach the stop conditions, and then sum everything back up. + It's really quite elegant. +

+

+ One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the + d(..., k) values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so + in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point + vector and then works its way up to that single point, rather than first starting "on the left", working our way "to the right" and then + summing back up "to the left". We can just start on the right and work our way left immediately. +

+

Running the computation

+

+ Unlike the de Casteljau algorithm, where the t value stays the same at every iteration, for B-Splines that is not the case, + and so we end having to (for each point we evaluate) run a fairly involving bit of recursive computation. The algorithm is discussed on + this Michigan Tech page, but an easier to read version + is implemented by b-spline.js, so we'll look at its code. +

+

+ Given an input value t, we first map the input to a value from the domain [0,1] to the domain + [knots[degree], knots[knots.length - 1 - degree]. Then, we find the section number s that this mapped + t value lies on: +

+ + + + + + + + + + + + +
1 + +
2
3
+ +

+ after running this code, s is the index for the section the point will lie on. We then run the algorithm mentioned on the MU + page (updated to use this description's variable names): +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1 + +
2
3
4
5
6
7
8
9
10
+ +

+ (A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at i=s at each level of the + interpolation, and we stop when i = s - order + level, so we always end up with a value for i such that those + v[i-1] don't try to use an array index that doesn't exist) +

+

Open vs. closed paths

+

+ Much like poly-Béziers, B-Splines can be either open, running from the first point to the last point, or closed, where the first and last + point are the same coordinate. However, because B-Splines are an interpolation of curves, not just points, we can't simply make the first + and last point the same, we need to link as many points as are necessary to form "a curve" that the spline performs interpolation with. As + such, for an order d B-Spline, we need to make the first and last d points the same. This is of course hardly + more work than before (simply append points.splice(0,d) to points) but it's important to remember that you need + more than just a single point. +

+

+ Of course if we want to manipulate these kind of curves we need to make sure to mark them as "closed" so that we know the coordinate for + points[0] and points[n-k] etc. don't just happen to have the same x/y values, but really are the same + coordinate, so that manipulating one will equally manipulate the other, but programming generally makes this really easy by storing + references to points, rather than copies (or other linked values such as coordinate weights, discussed in the NURBS section) rather than + separate coordinate objects. +

+

Manipulating the curve through the knot vector

+

+ The most important thing to understand when it comes to B-Splines is that they work because of the concept of a knot vector. As + mentioned above, knots represent "where individual control points start/stop influencing the curve", but we never looked at the + values that go in the knot vector. If you look back at the N() and a() functions, you see that interpolations are based on + intervals in the knot vector, rather than the actual values in the knot vector, and we can exploit this to do some pretty interesting + things with clever manipulation of the knot vector. Specifically there are four things we can do that are worth looking at: +

+
    +
  1. we can use a uniform knot vector, with equally spaced intervals,
  2. +
  3. we can use a non-uniform knot vector, without enforcing equally spaced intervals,
  4. +
  5. we can collapse sequential knots to the same value, locally lowering curve complexity using "null" intervals, and
  6. +
  7. + we can form a special case non-uniform vector, by combining (1) and (3) to for a vector with collapsed start and end knots, with a + uniform vector in between. +
  8. +
+

Uniform B-Splines

+

+ The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire + curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or + [4,5,6,...,13,14,15], which defines the same intervals, or even [0,2,3,...,18,20,22], which also defines + the same intervals, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to + the curvature. +

+ + + Скрипти вимкнено + + + + + +

+ This is an important point: the intervals that the knot vector defines are relative intervals, so it doesn't matter if every + interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve. +

+

+ The problem with uniform knot vectors is that, as we need order control points before we have any curve with which we can + perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get + rid of these, by being clever about how we apply the following uniformity-breaking approach instead... +

+

Reducing local curve complexity by collapsing intervals

+

+ Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the + sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, + and curve continuity goes down, to the point where collapsing order knots creates a situation where all continuity is lost + and the curve "kinks". +

+ + + Скрипти вимкнено + + + + + +

Open-Uniform B-Splines

+

+ By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we can overcome the problem of the + curve not starting and ending where we'd kind of like it to: +

+

+ For any curve of degree D with control points N, we can define a knot vector of length N+D+1 in + which the values 0 ... D+1 are the same, the values D+1 ... N+1 follow the "uniform" pattern, and the values + N+1 ... N+D+1 are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector + [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences + that determine the curve shape. +

+ + + Скрипти вимкнено + + + + + +

Non-uniform B-Splines

+

+ This is essentially the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to + pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, other than + that any value knots[k+1] should be greater than or equal to knots[k]. +

+

One last thing: Rational B-Splines

+

+ While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's + "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a + ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a + control point carries, the closer to that point the spline curve will lie, a bit like turning up the gravity of a control point, just like + for rational Bézier curves. +

+ + + Скрипти вимкнено + + + + + +

+ Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the + NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, + the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an + important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in + arbitrary-precision 2D design due to the level of control a NURBS curve offers designers. +

+

+ While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform + Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS, + they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the + last. +

+

Extending our implementation to cover rational splines

+

+ The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and the extension to work in the + control point weights is fairly simple: we extend each control point from a point in its original number of dimensions (2D, 3D, etc.) to + one dimension higher, scaling the original dimensions by the control point's weight, and then assigning that weight as its value for the + extended dimension. +

+

For example, a 2D point (x,y) with weight w becomes a 3D point (w * x, w * y, w).

+

+ We then run the same algorithm as before, which will automatically perform weight interpolation in addition to regular coordinate + interpolation, because all we've done is pretended we have coordinates in a higher dimension. The algorithm doesn't really care about how + many dimensions it needs to interpolate. +

+

+ In order to recover our "real" curve point, we take the final result of the point generation algorithm, and "unweigh" it: we take the + final point's derived weight w' and divide all the regular coordinate dimensions by it, then throw away the weight + information. +

+

+ Based on our previous example, we take the final 3D point (x', y', w'), which we then turn back into a 2D point by computing + (x'/w', y'/w'). And that's it, we're done! +

+
+
+ + +

+ + Comments and questions +

+

+ First off, 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 have two options: you can either head on over to the + Patreon page for this book, or if you prefer to make a one-time donation, head on over to + the buy Pomax a coffee page. This + work has grown from a small primer to a 70-plus print-page-equivalent reader on the subject of Bézier curves over the years, 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. +

+

With that said, on to the comments!

+
+
+
+
+ +
+ + + + + + diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index 67bbe064..00edcd98 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -13,6 +13,14 @@ + + @@ -33,7 +41,7 @@ - + @@ -126,9 +134,9 @@
Read this in your own language:

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

diff --git a/src/build.js b/src/build.js index c57ae5d5..0ec837a5 100644 --- a/src/build.js +++ b/src/build.js @@ -15,14 +15,18 @@ getAllChapterFiles().then(async (chapterFiles) => { const start = Date.now(); const languageCodes = Object.keys(chapterFiles); - await Promise.all( + const build = await Promise.all( languageCodes.map(async (locale) => { const localeStrings = new LocaleStrings(locale); - const chapters = await processLocale(locale, localeStrings, chapterFiles); - return createIndexPages(locale, localeStrings, chapters); + return await processLocale(locale, localeStrings, chapterFiles); }) ); + const progress = {}; + build.forEach((e) => (progress[e.locale] = e.percentage)); + + await Promise.all(build.map((e) => createIndexPages(e.locale, e.localeStrings, e.chapters, progress))); + await createNewsPages(); const runtime = Date.now() - start; diff --git a/src/build/create-index-pages.js b/src/build/create-index-pages.js index 6d4e15d1..94649475 100644 --- a/src/build/create-index-pages.js +++ b/src/build/create-index-pages.js @@ -11,10 +11,10 @@ nunjucks.configure(paths.html, { autoescape: false }); /** * ...docs go here... */ -async function createIndexPages(locale, localeStrings, chapters) { +async function createIndexPages(locale, localeStrings, chapters, langProgress) { const defaultLocale = localeStrings.getDefaultLocale(); const base = locale !== defaultLocale ? `` : ``; - const langSwitcher = generateLangSwitcher(localeStrings); + const langSwitcher = generateLangSwitcher(localeStrings, langProgress); const toclist = {}; const localePrefix = base ? `${locale}/index.html` : ``; diff --git a/src/build/generate-lang-switcher.js b/src/build/generate-lang-switcher.js index 8ccdfc45..d1891efb 100644 --- a/src/build/generate-lang-switcher.js +++ b/src/build/generate-lang-switcher.js @@ -2,7 +2,7 @@ * ...docs go here... * @param {*} localeStrings */ -export default function generateLangSwitcher(localeStrings) { +export default function generateLangSwitcher(localeStrings, langProgress) { const defaultLocale = localeStrings.getDefaultLocale(); return localeStrings @@ -15,7 +15,7 @@ export default function generateLangSwitcher(localeStrings) { link = `./${locale}/index.html`; } let localeName = localeStrings.getLocaleName(locale); - return `
  • ${localeName}
  • `; + return `
  • ${localeName} (${langProgress[locale]}%)
  • `; }) .join(`\n`); } diff --git a/src/build/process-locale.js b/src/build/process-locale.js index a0cf5613..a18a6448 100644 --- a/src/build/process-locale.js +++ b/src/build/process-locale.js @@ -52,9 +52,14 @@ async function processLocale(locale, localeStrings, chapterFiles) { }) ); - logRunInformation(locale, defaultLocale, missing, localized, sectionList, start); + const percentage = logRunInformation(locale, defaultLocale, missing, localized, sectionList, start); - return chapters; + return { + chapters, + locale, + localeStrings, + percentage, + }; } export { processLocale }; @@ -65,11 +70,14 @@ export { processLocale }; function logRunInformation(locale, defaultLocale, missing, localized, sectionList, start) { if (locale === defaultLocale && missing > 0) { console.log(`Warning: ${missing} chapters appear to be missing, based on the ToC listing.`); + return 0; } else { if (localized < sectionList.length) { console.log(`${locale} partially localized: [${localized}/${sectionList.length}]`); + return Math.round((100 * localized) / sectionList.length); } else { console.log(`${locale} fully localized.`); + return 100; } } diff --git a/src/html/fragments/fonts.html b/src/html/fragments/fonts.html new file mode 100644 index 00000000..a05af5c2 --- /dev/null +++ b/src/html/fragments/fonts.html @@ -0,0 +1,43 @@ + diff --git a/src/html/index.template.html b/src/html/index.template.html index c10e44f4..442e4b9b 100644 --- a/src/html/index.template.html +++ b/src/html/index.template.html @@ -15,6 +15,7 @@ + {% include "./fragments/fonts.html" %} {% include "./fragments/meta.html" %}