Bézier curves in Haskell

I have always been fascinated by the simplicity and versatility of the pen tool in Adobe Illustrator.

pen tool gif

The tool support creation and editing of bézier curves using a simple user interaction. Illustrator support linear, quadratic and cubic bézier curves.

A straight line is a linear bézier curve.

linear bézier curve

A line with one external point is quadratic bézier curve.

quadratic bézier curve

A line with two external points is cubic bézier curve.

cubic bézier curve

The way Illustrator support creation and editing of bézier curves is to put a handle on each line end point. On a cubic curve one handle is located on point P1 and one on point P2.

cubic bézier curve

different bézier curves

A curve with a single handle represent a quadratic bézier curve and a line with no handles a linear.

Haskell implementation

First I would like to implement bézier curve approximation in Haskell.

The implementation is quite straight forward. The bezier function defines one basic implementation for a linear bézier curve.

Quadratic or higher bézier can be calculated by defining a new set of lines between the bézier points of each line segment and recursively calculating the bézier of that line set, until we only have a single line left. See animations above.

For instance for a cubic bézier you have 3 line segments (P01, P12, P23). Two lines is defined by connecting the bézier points on these three lines which is used to calculate a quadratic bézier. The quadratic bézier can in the same way be reduced to a linear (which we have implemented).

module Main where

import Graphics.Gloss
import qualified Graphics.Gloss.Data.Point.Arithmetic as G
import Graphics.Gloss.Data.Vector
import Prelude hiding (lines)

main :: IO ()
main = do display (InWindow "Bezier" (400, 400) (20, 20)) black $ pic

type Bezier = [Point]

pairs :: [a] -> [(a,a)]
pairs xs = zip xs (drop 1 xs)

-- Generalized higher order Bézier approximation in a single point u [0..1]
bezier :: Bezier -> Float -> Point
bezier (p1:p2:[]) u = p1 G.+ (u G.* (p2 G.- p1))                            -- linear
bezier ps         u = let ps' = fmap (\(a,b) -> bezier [a,b] u) $ pairs ps  -- quadratic or higher
                      in  bezier ps' u

-- Draw approximate Bézier 
renderBezier :: Bezier -> Picture
renderBezier b = let len = foldl (\s (a,b) -> s + magV (b G.- a)) 0 $ pairs b
                     num = floor (len / 5)      -- resolution about 5 pixels
                     steps = fmap ((*) (1/(fromIntegral num)) . fromIntegral) [0..num]
                 in line $ fmap (bezier b) steps

-- Draw Bézier including control points
showBezier :: Bezier -> Picture
showBezier b = pictures [ Color green $ renderBezier b          -- curve
                        , Color orange $ line [head b, last b]  -- linear
                        , Color (greyN 0.75) $ line b]          -- control points

pic :: Picture
pic = Color white
    $ showBezier [(0,0),(0,100),(250,200),(200,0)]

The resulting screenshot can be found below.

bezier.png

UI for manipulating Bézier curves

The following table summarize a minimal set of actions to allow creation and modification of Bézier curves.

State       | Mouse Loc.   | Action            | Description                    | New state
----------- | ------------ | ----------------- | ------------------------------ | ---------
Select      | Empty space  | Click             | Start path with (no handles)   | Draw
"           | Empty space  | Click-drag        | Start path (handles)           | Draw
Draw        | Empty space  | Click             | Add point to path (no handles) | -
"           | Empty space  | Click-drag        | Add point to path (handles)    | -
"           | -            | Escape            | Undo last edit                 | -
Draw/Select | Handle       | Click-drag        | Modify handles on path [^1]    | -
"           | Handle       | Alt + Click-drag  | Modify single handle on path   | -
"           | Point        | Click-drag        | Modify point on path           | -
Draw        | Empty space  | Right-Click       | Finish path                    | Select
Select      | Point        | Alt + Click       | Toggle point in selection      | -
"           | Point        | Alt2 + Click-drag | Add handles to point           | -
"           | Path segmemt | Alt + Click       | Toggle path in selection       | -
"           | Path segmemt | Alt2 + Click      | Insert/remove point on path    | -
"           | Selection    | Click-drag        | Move selection                 | -
"           | Empty space  | Click             | Clear selection                | -
"           | -            | Delete            | Delete selection               | -

[^1]: Handle may be dropped on a point to remove the handle

TODO: Support square select/deselect in Select mode

See dotgrid and ronin for interesting implementations.

References