Splitting a cubic Bézier curve

In this post, I thought I'd go over how to split Bézier curves into an arbitrary number of smaller Bézier curves. Before diving into the nitty gritty, let's take a step back for a little context:

Let's write a very simple button component where we have a background animation.

There's a very straight-forward hover animation, with a pseudo element that has a transform: translateX(); applied on hover. There's also a simple ease-out applied to the transition so it's nice a smooth.

Unfortunately, things quickly break when you have two or more lines:

The first CSS property that came to mind was box-decoration-break: clone;. Unfortunately I needed the display element to be a block, or an inline-block. So this wouldn't work.

The next solution was to split the button's text into smaller elements and to apply multiple transition-delay to each, to stagger the animation – it's also worth noting that the transition-delay needs to be applied inversely to element:hover {} so the first element animates last when you hover out. (note: I've used cubic-bezier(0,0,0,1) to highlihght the issue)

As you can see, because the ease is applied on a per-element basis, the momentum of the animation is lost.

Below is a visual representation of how the ease is currently applied (in red), versus how we'd want to have it applied (in blue).

This brings me to the topic at hand: splitting Bézier curves. We want to find a transition-timing-function to be applied to each element, such that the overall animation respects the easing.

Below, we have a quadratic Bézier curve: two points where the line starts/end and two control points that dictates the curvature of the curve (control points).

Here's the quadratic Bézier curve function that returns the point of the curve at t (where t ∈ {0, 1}) given the four points p0, p1, p2, p3:

function CubicBezierCurve(t, p0, p1, p2, p3) { return (1 - t)³ * p0 + 3 * (1 - t)² * t * p1 + 3 * (1 - t) * t² * p2 + t³ * p3 }

Unfortunately, we cannot solely rely on this formula as it only gives us the point on the curve at a given t, without its two control points.

Luckily, there is a simple recursive approximate algorithm called de Casteljau's algorithm, which allows you to split a curve into two distinct Bézier curves at any given t.

Here's the geometric representation of the algorithm:

t = 0.1
Actual x-position ≅ 0

We're almost there. Given the text will vary in length, we'll need to split the Bézier curve on the y-axis. And here's the tricky part: how to find all the intersecting points between the line and the Bézier curve? Luckily, the heavy lifting was already done here

Below we have our tweaked component which has the transition-timing-function, transition-delay etc, tweaked and applied on a per element basis. The red line is a pseudo element on the button, with the same transition-timing-function: cubic-bezier(0,0,0,1);