It’s possible to analytically find an axis-aligned bounding box (AABB) for Bézier curves. I wanted to try doing that math for myself,1 and ended up writing a solver in Haskell, along with a few animations to show it off. All animations here were built with Reanimate.
In Reanimate, we can tween the value of a Double
variable x
over the course of time t
using
tweenVar
and a Signal
.
$ tweenVar x t $ \orig -> fromToS orig new . curveS 2 fork
This lets us cycle through a bunch of different curves by interpolating between the sets of variables that define them.
A cubic Bézier curve is defined by four points—call them
start
, end
, control start
, and
control end
. We can use these points to define an SVG that
will draw our Bézier curve for us. In Reanimate this is done with the
CurveTo
instruction on a Path
.
We can also draw the control points as little circles if we want to
visualize them. Defining these as sprites lets us use Var
s
to control them, and then Reanimate will handle all the variable
updating behind the scenes for us, which is pretty nice for focusing on
the math.
cubicBezier :: V2 Double
-> V2 Double
-> V2 Double
-> V2 Double
-> SVG
cubicBezierV2 startX startY)
(V2 controlStartX controlStartY)
(V2 controlEndX controlEndY)
(V2 endX endY)
(=
PathTree $ Path mempty
MoveTo OriginAbsolute [V2 startX startY]
[ CurveTo OriginAbsolute
,
[V2 controlStartX controlStartY
( V2 controlEndX controlEndY
, V2 endX endY
,
)
]
]
point :: Double -> Double -> SVG
=
point x y CircleTree $ Circle mempty (Num x, Num y) (Num 0.1)
In two dimensions, an axis-aligned bounding box is defined by four
numbers—min X
, min Y
, max X
and
max Y
(this is equivalent to using two corner points).
We can find these numbers by solving the quadratic formula to end up
with time values t
to test, then plugging these
t
s back into our cubic Bézier to find minima and
maxima.
These equations give us the a
b
and
c
that we’ll be plugging into the quadratic formula, and
the overall strategy comes from taking the derivative of our cubic curve
to find its roots (the derivative of a cubic is a quadratic). We can
apply these equations independently to the X and Y parts of our four
points that define the Bézier curve.2
=
sol_a start controlStart controlEnd end 3 * (-start + 3 * controlStart - 3 * controlEnd + end)
=
sol_b start controlStart controlEnd 6 * (start - 2 * controlStart + controlEnd)
=
sol_c start controlStart 3 * (controlStart - start)
We need to be slightly careful here to return only valid roots and
only valid values of t
. Since t
ranges from 0
to 1, we throw out anything outside that range, as well as ignoring
cases where we would otherwise perform division by zero.
=
quadratic a b c if a == 0 then []
else filter (\t -> t > 0 && t < 1)
-b + radicand) / (2 * a)
[ (-b - radicand) / (2 * a)
, (
]where radicand = sqrt (b * b - 4 * a * c)
We now have a bunch of options for t
values to try,
including 0
and 1
(the start and end points of
the curve, respectively).
= [0, 1] <> xRoots <> yRoots allTsToTry
We can plug that list back into the cubic Bézier equation to find extreme points, e.g. the minimum X value. Again we can treat X and Y separately.
=
solveCubicBezierX t * (1 - t) * (1 - t) * (1 - t) + 3 * controlStart * t * (1 - t) * (1 - t) + 3 * controlEnd * t * t * (1 - t) + end * t * t * t
start
= minimum $ map solveCubicBezierX allTsToTry minX
Typing out that formula was where I made this project’s mistake that
was hardest to debug: I should have written the second line here instead
of the top for controlStart
.
* (1 - t) * (1 - t)
t * t * (1 - t) t
After spotting that, we now have a nice Bézier-in-a-box, solved entirely analytically, and complete with animations.
Because of the amount of math involved, I think Haskell was an
excellent choice for this little project. Animating by hand would have
been painful. Reanimate took some getting used to, but once I realized I
could write my math functions as plainly as possible, but make use of
the Applicative
instance for Frame
s to get
tweening almost “for free”, it got a lot nicer.