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.

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 – and t doesn't directly correlate to the x-axis.

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 visual representation of the algorithm (the vertical line tracks t):

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 (as opposed to the x-axis which is the easing duration). And this is where it gets a bit tricky: as you can see in the graph below, if we were to split the Bézier curve on the y-axis, in three separate parts, we'd end-up with more than one intersection between the horizontal lines and the curve:

Luckily, finding the intersection of a line and a Bézier curve is solved here.

👀 Note to self: finish writing this.

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);