Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
Bezier curves.
Synopsis
- data Bezier = Bezier !Vec2 !Vec2 !Vec2 !Vec2
- bezierLength :: Bezier -> Double
- bezierParametric :: Bezier -> Double -> Vec2
- bezierArcParametric :: Bezier -> Double -> Double -> Vec2
- bezierSubdivideEquiparametric :: Int -> Bezier -> [Vec2]
- bezierSubdivideEquidistant :: Int -> Bezier -> [Vec2]
- bezierSubdivideCasteljau :: Double -> Bezier -> [Vec2]
- bezierSmoothen :: Sequential vector => vector Vec2 -> Vector Bezier
Documentation
Cubic Bezier curve, defined by start, first/second control points, and end.
Instances
Show Bezier Source # | |
NFData Bezier Source # | |
Defined in Geometry.Bezier | |
Sketch Bezier Source # | (image code)
|
Plotting Bezier Source # | FluidNC doesn’t support G05, so we approximate Bezier curves with line pieces.
We use the naive Bezier interpolation |
ChaosSource Bezier Source # | |
MwcChaosSource Bezier Source # | |
HasBoundingBox Bezier Source # | (image code)
|
Defined in Geometry.Bezier boundingBox :: Bezier -> BoundingBox Source # | |
Transform Bezier Source # | |
Defined in Geometry.Bezier | |
Eq Bezier Source # | |
Ord Bezier Source # | |
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
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 \]
:: 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
Subdividing
bezierSubdivideEquiparametric Source #
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 #
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)
>>>
:{
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)
>>>
:{
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)
>>>
:{
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
- Moving Along a Curve with Specified Speed (2019) by David Eberly https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
Smoothening
- Cubic Bézier Splines by Michael Joost https://www.michael-joost.de/bezierfit.pdf
- Building Smooth Paths Using Bézier Curves by Stuart Kent https://www.stkent.com/2015/07/03/building-smooth-paths-using-bezier-curves.html