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.
This youtube video was what kicked off my curiosity here about bounded Bézier curves. Its animations are really high quality, which kicked off my desire to try using Reanimate for this project. Quite the inspiring video!↩︎
I found this Game Dev StackExchange post helpful as a way to explain this process and check my answers.↩︎