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