Matthias Dittgen

December 3, 2021

Bezier curves

Well, here’s one more post about a more general concept in computer graphics.

Sure, you will find a number of articles about Bezier curves on the Internet and you can certainly check out a few YouTube videos. Therefore, I will not go too deep into the mathematics here, but try to go into how easy it is to create the curves and provide them with interaction if you use SVG and Svelte for this.

Today SVG is a first class citizen in HTML5 and rendered by all major browsers. It is a declarative approach to define Vector Graphics. This plays nice with Svelte, which allows us build reactive components and bind dynamically changed values to e.g. the d=""Attribute of SVG’s <path /> element.

Lines»

Before we come to curves, let’s recap my last post on linear interpolation. With the help of lerp() we were able to find a point along a line between two other points.

t: 0.50 t: 0.50

In the demo here you can move the starting point and ending point using drag & drop.

I will not explain the DragPoint-component in more detail, but you are welcome to take a closer look at the linked Svelte REPL.

<script>
  // import statements omitted
  let svg;
  let t = 0.5;

  let s = point(50, 75);
  let e = point(350, 25);

  $: lineS_E = linePath(s, e);
  $: se = plerp(s, e, t);
  $: lineS_SE = linePath(s, se);
</script>

<RangeSlider label="t" bind:value={t} step="0.01" min="0" max="1" />

<SvgContaiiner bind:svg={svg} height="150">
  <path d={lineS_E} stroke-width="1" stroke="#cccccc" fill="none" />
  <path d={lineS_SE} stroke-width="2" stroke="#999999" fill="none" />
  <Point bind:coord={se} radius="8" color={"#999999"} />
  <DragPoint svg={svg} bind:coord={s} color={"hsl(120, 75%, 75%)"} />
  <DragPoint svg={svg} bind:coord={e} color={"hsl(360, 75%, 75%)"} />
</SvgContaiiner>

The code is explained quickly.

  let s = point(50, 75);
  let e = point(350, 25);

We have the coordinates s and e for the two points with initial values. Using bind:coord={x}, these coordinates are linked to the DragPoint-component. The Svelte Compiler does this for us, so at runtime there’s a two-way data binding between this variable and the components. Whenever the value changes, either in this component (one way) or in the DragPoint-component (the other way around), it is updated at the other place.

  $: lineS_E = linePath(s, e);
  $: se = plerp(s, e, t);
  $: lineS_SE = linePath(s, se);

The expressions beginning with $: are reactive computations. If a value to the right of the equal sign changes, the value is recalculated and the variable on the left changes its value.

In this way we compute:

And Svelte again takes care that whenever the values change, the d-attribute to <path>, as well as the coord-attribute of the <Point>-component are updated accordingly. That’s it!

Curves»

Now we come to the curves. You can find out a lot about the history of the curves and learn that in the early 1960s Pierre Bézier at Renault and Paul de Casteljau at Citroën independently found out about these curves. A good starting point is usually Wikipedia

A bezier curve can be described with just a few points. The reconstruction of the curve from these points, i.e. the calculation of all points along the curve, can then be done using a formula based on the Bernstein polynomials, as well as using the De-Casteljau Algorithm. This algorithm is what we’ll focus on. It traces the calculation of a curve point back to repeated linear interpolation. And we can now easily demonstrate that again with SVG and the Reactivity of Svelte.

Quadratic bezier curve»

Let’s start with a quadratic beziers. My first contact to these were back in the days when I called myself a flash developer. Also the Macromedia or later Adobe Flash Tool internally used these in the player, but used cubic beziers in the tool itself.

t: 0.50 t: 0.50

A quadratic bezier is defined by only three point, the starting (S) and ending (E) point on the curve and a single control point (c) off the curve.

  let s = point(50, 225);
  let c = point(200, 25);
  let e = point(350, 225);
  $: lineS_C_E =
    linePath(s, c) +
    linePath(c, e);

We compute the lines SC and CE and lerp()ing points along those. We connect the two points (light gray) with a line.

  $: sc = plerp(s, c, t);
  $: ce = plerp(c, e, t);
  $: lineSC_CE = linePath(sc, ce);

And along this line we are lerp()ing again another point (dark gray). All points calculated this way will draw the quadratic bezier curve.

  $: sce = plerp(sc, ce, t);
  $: curve = quadCurvePath(s, sc, sce);

But we just draw the part of the curve at t using the quadrativ bezier command from SVG. For this we built an appropriate d=-attribute for the SVG <path/> element.

const moveTo = (p) =>
  `M ${p.x} ${p.y}`;

const quadTo = (c, p) =>
  `Q ${c.x} ${c.y} ${p.x} ${p.y}`;

const quadCurvePath = (s, c, e) =>
  moveTo(s) + ' ' + quadTo(c, e);

And here’s the full code for the interative demo from above.

<script>
  // import statements omitted
  let svg;
  let t = 0.5;

  let s = point(50, 225);
  let c = point(200, 25);
  let e = point(350, 225);
  $: lineS_C_E = linePath(s, c) + linePath(c, e);

  $: sc = plerp(s, c, t);
  $: ce = plerp(c, e, t);
  $: lineSC_CE = linePath(sc, ce);

  $: sce = plerp(sc, ce, t);
  $: curve = quadCurvePath(s, sc, sce);
</script>

<RangeSlider label="t" bind:value={t} step="0.01" min="0" max="1" />

<SvgContaiiner bind:svg={svg} height={250}>
  <path d={lineS_C_E} stroke-width="1" stroke="#cccccc" fill="none" />
  <path d={lineSC_CE} stroke-width="1.5" stroke="#cccccc" fill="none" />
  <path d={curve} stroke-width="2" stroke="#999999" fill="none" />
  <Point bind:coord={sc} radius="8" color={"#cccccc"} />
  <Point bind:coord={ce} radius="8" color={"#cccccc"} />
  <Point bind:coord={sce} radius="8" color={"#999999"} />
  <DragPoint svg={svg} bind:coord={s} color={"hsl(120, 75%, 75%)"} />
  <DragPoint svg={svg} bind:coord={c} color={"hsl(240, 75%, 75%)"} />
  <DragPoint svg={svg} bind:coord={e} color={"hsl(360, 75%, 75%)"} />
</SvgContaiiner>

Cubic bezier curve»

t: 0.50 t: 0.50

I won’t explain the cubic curve in full detail again, but as you can see, there’s one more control point off the curce. So we need one more iteration of lerp()ing.

First we habe three lines, then two, then one and eventually we interpolate a point at the cubic bezier.

With cubic bezier curves you can come up with more complex curves with just one more control point. Try to drag & drop the DragPoints in the demo over here:

You can also notice, that now that we know, how to determine these helper points, we already have all it takes to split quadartiv or cubic bezier curves.

Here’s the source code of the demo:

<script>
  // import statements omitted
  let svg;
  let t = 0.5;

  let s = point(50, 225);
  let a = point(150, 25);
  let b = point(250, 25);
  let e = point(350, 225);
  $: lineS_A_B_E = linePath(s, a) + linePath(a, b) + linePath(b, e);

  $: sa = plerp(s, a, t);
  $: ab = plerp(a, b, t);
  $: be = plerp(b, e, t);
  $: lineSA_AB_BE = linePath(sa, ab) + linePath(ab, be);

  $: sab = plerp(sa, ab, t);
  $: abe = plerp(ab, be, t);
  $: lineSAB_ABE = linePath(sab, abe);

  $: sabe = plerp(sab, abe, t);
  $: curve = cubicCurvePath(s, sa, sab, sabe);
</script>

<RangeSlider label="t" bind:value={t} step="0.01" min="0" max="1" />

<SvgContaiiner bind:svg={svg} height={250}>
  <path d={lineS_A_B_E} stroke-width="1" stroke="#cccccc" fill="none" />
  <path d={lineSA_AB_BE} stroke-width="1" stroke="#cccccc" fill="none" />
  <path d={lineSAB_ABE} stroke-width="1" stroke="#999999" fill="none" />
  <path d={curve} stroke-width="2" stroke="#aaaaaa" fill="none" />
  <Point bind:coord={sa} radius="8" color={"#cccccc"} />
  <Point bind:coord={ab} radius="8" color={"#cccccc"} />
  <Point bind:coord={be} radius="8" color={"#cccccc"} />
  <Point bind:coord={sab} radius="8" color={"#999999"} />
  <Point bind:coord={abe} radius="8" color={"#999999"} />
  <Point bind:coord={sabe} radius="8" color={"#666666"} />
  <DragPoint svg={svg} bind:coord={s} color={"hsl(120, 75%, 75%)"} />
  <DragPoint svg={svg} bind:coord={a} color={"hsl(200, 75%, 75%)"} />
  <DragPoint svg={svg} bind:coord={b} color={"hsl(280, 75%, 75%)"} />
  <DragPoint svg={svg} bind:coord={e} color={"hsl(360, 75%, 75%)"} />
</SvgContaiiner>

Mesh»

You might think, nice to know how bezier curves are constructed. But SVG already offers a way to draw these curves, so why should I care?

Well there is some beauty in the pattern that was used to construct the curves. I call it a mesh and Spiderman would surely love it. Feel free to play with the DragPoints again an see how the mesh changes.

And maybe you are like me and you can no longer overlook when you see such a mesh or a bezier curve on digital artworks at social media.

This post about curves, just like the one about linear interpolation, was only created for the next post about… well, wait for it.