generative-art-0.1.0.0
Safe HaskellSafe-Inferred
LanguageHaskell2010

Geometry.Bezier

Description

Bezier curves.

Synopsis

Documentation

data Bezier Source #

Cubic Bezier curve, defined by start, first/second control points, and end.

Constructors

Bezier !Vec2 !Vec2 !Vec2 !Vec2 

Instances

Instances details
Show Bezier Source # 
Instance details

Defined in Geometry.Bezier

NFData Bezier Source # 
Instance details

Defined in Geometry.Bezier

Methods

rnf :: Bezier -> () #

Sketch Bezier Source #

(image code)

Expand
>>> :{
haddockRender "Draw/instance_Sketch_Bezier.svg" 150 100 $ \_ -> do
    C.setLineWidth 2
    sketch (Bezier (Vec2 10 10) (Vec2 50 200) (Vec2 100 (-50)) (Vec2 140 90))
    stroke
:}
Generated file: size 2KB, crc32: 0xe17dab02
Instance details

Defined in Draw

Methods

sketch :: Bezier -> Render () Source #

Plotting Bezier Source #

FluidNC doesn’t support G05, so we approximate Bezier curves with line pieces. We use the naive Bezier interpolation bezierSubdivideEquiparametric, because it just so happens to put more points in places with more curvature.

Instance details

Defined in Draw.Plotting

Methods

plot :: Bezier -> Plot () Source #

ChaosSource Bezier Source # 
Instance details

Defined in Geometry.Chaotic

Methods

perturb :: Bezier -> Int Source #

MwcChaosSource Bezier Source # 
Instance details

Defined in Geometry.Chaotic

HasBoundingBox Bezier Source #

(image code)

Expand
>>> :{
haddockRender "Geometry/Bezier/bounding_box_bezier.svg" 150 100 $ \_ -> do
    let curve = let curveRaw = transform (rotate (deg (-30))) (Bezier (Vec2 0 0) (Vec2 1 5) (Vec2 2.5 (-1)) (Vec2 3 3))
                    fitToBox = transform (transformBoundingBox curveRaw (shrinkBoundingBox 10 [zero, Vec2 150 100]) (TransformBBSettings FitWidthHeight IgnoreAspect FitAlignCenter))
                in fitToBox curveRaw
    cairoScope $ do
        setColor (mma 1)
        C.setDash [1.5,3] 0
        sketch (boundingBox curve)
        C.stroke
    cairoScope $ do
        C.setLineWidth 2
        sketch curve
        C.stroke
:}
Generated file: size 2KB, crc32: 0x73983106
Instance details

Defined in Geometry.Bezier

Transform Bezier Source # 
Instance details

Defined in Geometry.Bezier

Eq Bezier Source # 
Instance details

Defined in Geometry.Bezier

Methods

(==) :: Bezier -> Bezier -> Bool #

(/=) :: Bezier -> Bezier -> Bool #

Ord Bezier Source # 
Instance details

Defined in Geometry.Bezier

General properties

bezierLength :: Bezier -> Double Source #

Length of a Bézier curve by integration using Simpson’s Rule. It’s comparably slow, but very precise.

If speed is more important than quality, simply sum up a segmented subdivision. 16 subdivisions is often enough for a reasonably good result:

>>> curve = Bezier (Vec2 0 0) (Vec2 1 5) (Vec2 2.5 (-1)) (Vec2 3 3)
>>> bezierLength curve
5.645...
>>> polylineLength . Polyline . bezierSubdivideEquiparametric 16 $ curve
5.612...

Indexing

bezierParametric Source #

Arguments

:: Bezier 
-> Double

\([0\ldots 1] = [\mathrm{start}\ldots\mathrm{end}]\)

-> Vec2 

Point on a Bezier curve.

\[ \text{bezierParametric}_{\mathbf a,\mathbf b,\mathbf c,\mathbf d}(t) = (1-t)^3\,\mathbf a + 3 (1-t)^2 t\,\mathbf b + 3 (1-t) t^2\,\mathbf c + t^3\,\mathbf d \]

bezierArcParametric Source #

Arguments

:: Bezier 
-> Double

Precision parameter (smaller is more precise but slower).

-> Double

Distance to walk on the curve. Clips (stops at the end) when asked to »walk too far«.

-> Vec2

Point at that distance

Get the position on a Bezier curve as a fraction of its length, via solving a differential equation. This is much more expensive to compute than bezierParametric.

This caches the internal LUT when partially applied, so that the following will only compute it once for repeated lookups:

let walkOnBezier = bezierArcParametric bezier 0.01
print [walkOnBezier d | d <- [0, 0.1 .. 5]]

Subdividing

bezierSubdivideEquiparametric Source #

Arguments

:: Int

Number of segments

-> Bezier 
-> [Vec2] 

Trace a Bezier curve with a number of points, using the polynomial curve parameterization. This is very fast, but leads to unevenly spaced points.

For subdivision by arc length, use bezierSubdivideEquidistant.

bezierSubdivideEquidistant Source #

Arguments

:: Int

Number of segments

-> Bezier 
-> [Vec2] 

Trace a Bezier curve with a number of evenly spaced points by arc length. This is much more expensive than bezierSubdivideEquiparametric, but may be desirable for aesthetic purposes.

Here it is alongside bezierSubdivideEquiparametric:

(image code)

Expand
>>> :{
haddockRender "Geometry/Bezier/subdivide_s_t_comparison.svg" 300 150 $ \_ -> do
    let curve = let curveRaw = transform (rotate (deg (-30))) (Bezier (Vec2 0 0) (Vec2 1 5) (Vec2 2.5 (-1)) (Vec2 3 3))
                    fitToBox = transform (transformBoundingBox curveRaw (Vec2 10 10, Vec2 290 90) (TransformBBSettings FitWidthHeight IgnoreAspect FitAlignCenter))
                in fitToBox curveRaw
        evenlySpaced = bezierSubdivideEquidistant 16 curve
        unevenlySpaced = bezierSubdivideEquiparametric 16 curve
        offsetBelow :: Transform geo => geo -> geo
        offsetBelow = transform (translate (Vec2 0 50))
    cairoScope $ do
        setColor $ mma 1
        sketch curve
        C.stroke
        sketch (offsetBelow curve)
        C.stroke
    for_ (zip evenlySpaced unevenlySpaced) $ \(e, u') -> do
        let u = offsetBelow u'
        let circle p = sketch (Circle p 3) >> C.stroke
            connect p q = do
                let line = resizeLineSymmetric (*0.8) (Line p q)
                sketch line
                C.stroke
        cairoScope (setColor (mma 0) >> circle e)
        cairoScope (setColor (mma 3) >> circle u)
        cairoScope (setColor (black `withOpacity` 0.2) >> connect e u)
:}
Generated file: size 17KB, crc32: 0x7c147951

bezierSubdivideCasteljau :: Double -> Bezier -> [Vec2] Source #

Approximage a Bezier curve with line segments up to a certain precision, using relatively few points.

The idea behind Casteljau subdivision is that each Bézier curve can be exactly subdivided into two Bézier curves (of same degree). This is done recursively, in this implementation (and commonly) in the middle of the curve. Once a curve segment is flat enough (given by the tolerance parameter), it is simply rendered as a line.

The algorithm is based on an excellent blogpost that is sadly only available on the internet archive these days.

(image code)

Expand
>>> :{
haddockRender "Geometry/Bezier/bezierSubdivideCasteljau.svg" 500 330 $ \_ -> do
    let curve = let curveRaw = transform (rotate (deg (-30))) (Bezier (Vec2 0 0) (Vec2 1 5) (Vec2 2.5 (-1)) (Vec2 3 3))
                    fitToBox = transform (transformBoundingBox curveRaw (shrinkBoundingBox 10 [zero, Vec2 500 200]) (TransformBBSettings FitWidthHeight IgnoreAspect FitAlignCenter))
                in fitToBox curveRaw
        paintOffset = Vec2 0 30
    for_ (zip [0..] [50,25,10,2]) $ \(i, tolerance) -> cairoScope $ do
        let points = bezierSubdivideCasteljau tolerance (transform (translate (fromIntegral i *. paintOffset)) curve)
        setColor (mma i)
        C.setLineWidth 2
        sketch (Polyline points) >> C.stroke
        for_ points $ \p -> sketch (Circle p 3) >> C.fill
    cairoScope $ do
        C.setLineWidth 3
        setColor black
        sketch (transform (translate (4*.paintOffset)) curve)
        C.stroke
:}
Generated file: size 20KB, crc32: 0x679b311c

Interpolation

bezierSmoothen :: Sequential vector => vector Vec2 -> Vector Bezier Source #

Smoothen a number of points by putting a Bezier curve between each pair. Useful to e.g. make a sketch nicer, or interpolate between points of a crude solution to a differential equation.

If the first and last point are identical, assume the trajectory is closed, and smoothly interpolate between beginning and end as well, yielding a nice loop.

This works even for input loops with at least three points (plus one duplicate for the looping condition start=end). (For smaller inputs the scale is arbitrary so these solutions are excluded here.)

(image code)

Expand
>>> :{
haddockRender "Geometry/Bezier/bezierSmoothen.svg" 400 300 $ \_ -> do
    let points = [ Vec2 100 275, Vec2 150 125, Vec2 300 275, Vec2 350 75
                 , Vec2 250 50, Vec2 75 75, Vec2 75 50, Vec2 225 100
                 , Vec2 100 275 ]
        prettyBezier bezier@(Bezier p0 p1 p2 p3) = do
            cairoScope $ do
                setColor black
                sketch bezier
                C.stroke
            cairoScope $ do
                setColor (mma 0)
                sketch (Circle p1 4) >> C.fill
                sketch (Line p0 p1) >> C.stroke
            cairoScope $ do
                setColor (mma 1)
                sketch (Circle p2 4) >> C.fill
                sketch (Line p3 p2) >> C.stroke
            cairoScope $ do
                sketch (Circle p0 5)
                setColor (mma 3)
                C.fillPreserve
                setColor black
                C.stroke
    C.setLineWidth 2
    for_ (bezierSmoothen points) prettyBezier
:}
Generated file: size 15KB, crc32: 0xb9f3566d
>>> :{
haddockRender "Geometry/Bezier/bezierSmoothen_tiny_loop.svg" 200 200 $ \_ -> do
    let points = [ Vec2 20 60
                 , Vec2 150 50
                 , Vec2 140 180
                 , Vec2 20 60 ]
    let prettyBezier bezier@(Bezier p0 p1 p2 p3) = do
            cairoScope $ do
                setColor black
                sketch bezier
                C.stroke
            cairoScope $ do
                setColor (mma 0)
                sketch (Circle p1 4) >> C.fill
                sketch (Line p0 p1) >> C.stroke
            cairoScope $ do
                setColor (mma 1)
                sketch (Circle p2 4) >> C.fill
                sketch (Line p3 p2) >> C.stroke
            cairoScope $ do
                sketch (Circle p0 5)
                setColor (mma 3)
                C.fillPreserve
                setColor black
                C.stroke
    C.setLineWidth 2
    for_ (bezierSmoothen points) prettyBezier
:}
Generated file: size 7KB, crc32: 0xca727498

References

Arc length parameterization

Smoothening