gelatin-0.1.0.0: src/Gelatin/Core/Bezier.hs
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- | Here is a simple bezier algebra.
-- To better server drawing beziers the 'Bezier' type's first
-- record is a boolean representing its fill direction (inner or outer).
module Gelatin.Core.Bezier (
-- * Types
Bezier,
QuadraticBezier,
CubicBezier,
-- * Smart Constructors
bezier,
bez3,
bez4,
-- * Special helpers
fmapBezier,
fmapQuadraticBezier,
fmapCubicBezier,
transformBezier,
transformQuadraticBezier,
transformCubicBezier,
triangleArea,
-- * Conversion
bezToBez3,
bez3ToBez,
bez3ToBezInner,
bez3ToBezOuter,
bez4ToBez,
bez4ToBezInner,
bez4ToBezOuter,
bez4sToPath,
flipBez4,
demoteCubic,
-- * Subdivision
deCasteljau,
subdivideAdaptive,
subdivideAdaptive3,
subdivideAdaptive4,
cleanSeqDupes,
-- * Shapes
arcBez4,
arcBez3,
ellipseBez4,
ellipseBez3,
cornerBez4,
cornerBez3
) where
import Gelatin.Core.Transform
import Linear
import qualified Data.Vector.Unboxed as V
import Data.Vector.Unboxed (Vector, Unbox)
-- | A bezier that fills internally or externally.
type Bezier a = (Bool, a, a, a)
-- | A simple quadratic bezier with no explicit fill direction.
type QuadraticBezier a = (a, a, a)
-- | A simple cubic bezier with no explicit fill direction.
type CubicBezier a = (a, a, a, a)
fmapBezier :: (a -> b) -> Bezier a -> Bezier b
fmapBezier f (o, a, b, c) = (o, f a, f b, f c)
fmapQuadraticBezier :: (a -> b) -> QuadraticBezier a -> QuadraticBezier b
fmapQuadraticBezier f (a, b, c) = (f a, f b, f c)
fmapCubicBezier :: (a -> b) -> CubicBezier a -> CubicBezier b
fmapCubicBezier f (a, b, c, d) = (f a, f b, f c, f d)
transformBezier :: Num a => M44 a -> Bezier (V2 a) -> Bezier (V2 a)
transformBezier = fmapBezier . transformV2
transformQuadraticBezier :: Num a => M44 a -> QuadraticBezier (V2 a)
-> QuadraticBezier (V2 a)
transformQuadraticBezier = fmapQuadraticBezier . transformV2
transformCubicBezier :: Num a => M44 a -> CubicBezier (V2 a)
-> CubicBezier (V2 a)
transformCubicBezier = fmapCubicBezier . transformV2
-- | Create a bezier primitive. The area of the triangle formed by the
-- bezier's three points will be used to determine the orientation.
bezier :: (Ord a, Fractional a) => V2 a -> V2 a -> V2 a -> Bezier (V2 a)
bezier a b c = (triangleArea a b c > 0, a, b, c)
triangleArea :: Num a => V2 a -> V2 a -> V2 a -> a
triangleArea (V2 x2 y2) (V2 x0 y0) (V2 x1 y1) =
(x1-x0)*(y2-y0)-(x2-x0)*(y1-y0)
--bezToPath :: (RealFloat a, Unbox a) => Bezier (V2 a) -> Path (V2 a)
--bezToPath = Path . subdivideAdaptive 100 0 . bezToBez3
-- | Create a quadratic bezier. This is an alias of 'QuadraticBezier'.
bez3 :: V2 a -> V2 a -> V2 a -> QuadraticBezier (V2 a)
bez3 = (,,)
-- | Convert a Bezier to a QuadraticBezier.
bezToBez3 :: Bezier a -> QuadraticBezier a
bezToBez3 (_,a,b,c) = (a,b,c)
-- | Create a cubic bezier. This is an alias of 'CubicBezier'.
bez4 :: V2 a -> V2 a -> V2 a -> V2 a -> CubicBezier (V2 a)
bez4 = (,,,)
flipBez4 :: CubicBezier a -> CubicBezier a
flipBez4 (a,b,c,d) = (d,c,b,a)
-- | Convert a quadratic bezier into a bezier primitive and derives the winding
-- (which determines drawing an inner or outer bez) from the order of control
-- points.
bez3ToBez :: (Ord a, Fractional a, Unbox a)
=> QuadraticBezier (V2 a) -> Bezier (V2 a)
bez3ToBez (a,b,c) = bezier a b c
-- | Convert a quadratic bezier into a bezier primitive that fills outer.
bez3ToBezOuter :: (Ord a, Fractional a, Unbox a)
=> QuadraticBezier (V2 a) -> Bezier (V2 a)
bez3ToBezOuter qbz =
case bez3ToBez qbz of
z@(True,_,_,_) -> z
(_,a,b,c) -> bezier c b a
-- | Convert a quadratic bezier into a bezier primitive that fills inner.
bez3ToBezInner :: (Ord a, Fractional a, Unbox a)
=> QuadraticBezier (V2 a) -> Bezier (V2 a)
bez3ToBezInner qbz =
case bez3ToBez qbz of
(True,a,b,c) -> bezier c b a
b -> b
-- | Convert a cubic bezier into a list of drawable bezier primitives.
bez4ToBez :: (Ord a, Fractional a, Unbox a)
=> CubicBezier (V2 a) -> Vector (Bezier (V2 a))
bez4ToBez = V.map bez3ToBez . demoteCubic
-- | Convert a cubic bezier into a list of drawable bezier primitives that
-- fill the inner bezier.
bez4ToBezInner :: (Ord a, Fractional a, Unbox a)
=> CubicBezier (V2 a) -> Vector (Bezier (V2 a))
bez4ToBezInner = V.map bez3ToBezInner . demoteCubic
-- | Convert a cubic bezier into a list of drawable bezier primitives that
-- fill the inner bezier.
bez4ToBezOuter :: (Ord a, Fractional a, Unbox a)
=> CubicBezier (V2 a) -> Vector (Bezier (V2 a))
bez4ToBezOuter = V.map bez3ToBezOuter . demoteCubic
-- | Convert a list of cubic beziers into a smooth path.
bez4sToPath :: (RealFloat a, Unbox a)
=> a -> a -> Vector (CubicBezier (V2 a)) -> Vector (V2 a)
bez4sToPath mScale mAngle =
cleanSeqDupes . V.concatMap (subdivideAdaptive4 mScale mAngle)
-- | Compute the point at `t` along an N-bezier curve.
deCasteljau :: (Additive f, R1 f, R2 f, Num a) => a -> [f a] -> f a
deCasteljau _ [b] = b
deCasteljau t coefs = deCasteljau t reduced
where reduced = zipWith (flip (lerp t)) coefs (Prelude.tail coefs)
curveCollinearityEpsilon :: Double
curveCollinearityEpsilon = 1e-30
curveAngleToleranceEpsilon :: Double
curveAngleToleranceEpsilon = 0.01
curveRecursionLimit :: Int
curveRecursionLimit = 32
-- | Approximate a cubic bezier with a list of four quadratic beziers.
--
-- <<docimages/demoteCubic.png>>
demoteCubic :: (Fractional a, Unbox a)
=> CubicBezier (V2 a) -> Vector (QuadraticBezier (V2 a))
demoteCubic (a,b,c,d) = V.fromList [q1,q2,q3,q4]
where mid = lerp 0.5
m1 = mid a b
m2 = mid b c
m3 = mid c d
m1m2 = mid m1 m2
m2m3 = mid m2 m3
mam1 = mid a m1
h1 = mid mam1 m1
p2 = mid m1m2 m2m3
m1m2p2 = mid m1m2 p2
h2 = mid m1m2 m1m2p2
mp2m2m3 = mid p2 m2m3
h3 = mid mp2m2m3 m2m3
mdm3 = mid d m3
h4 = mid mdm3 m3
p1 = mid h1 h2
--p2 = mid h2 h3
p3 = mid h3 h4
q1 = bez3 a h1 p1
q2 = bez3 p1 h2 p2
q3 = bez3 p2 h3 p3
q4 = bez3 p3 h4 d
-- | Adaptively subdivide the quadratic bezier into a series of points (line
-- segments).
-- i.e. Generate more points along the part of the curve with greater curvature.
-- @see http://www.antigrain.com/research/adaptive_bezier/index.html
-- and http://www.antigrain.com/__code/src/agg_curves.cpp.html
subdivideAdaptive,subdivideAdaptive3 :: (RealFloat a, Unbox a)
=> a -> a -> QuadraticBezier (V2 a)
-> Vector (V2 a)
subdivideAdaptive3 = subdivideAdaptive
subdivideAdaptive mScale mAngle (va,vb,vc) =
let mDistanceToleranceSquare = (0.5 / mScale) ** 2
vs = subdivide mDistanceToleranceSquare mAngle 0 va vb vc
in va `V.cons` vs V.++ V.singleton vc
-- | Adaptively subdivide the cubic bezier into a series of points (line
-- segments).
subdivideAdaptive4 :: (RealFloat a, Unbox a)
=> a -> a -> CubicBezier (V2 a) -> Vector (V2 a)
subdivideAdaptive4 s a = cleanSeqDupes . V.concatMap (subdivideAdaptive s a) . demoteCubic
-- | Removes sequential duplicates from a vector.
cleanSeqDupes :: (Eq a, Unbox a) => Vector a -> Vector a
cleanSeqDupes vs
| V.length vs > 1 = vs1 `V.snoc` V.last vs
| otherwise = vs
where vs1 = V.map fst $ V.filter (uncurry (/=)) $ V.zip vs (V.drop 1 vs)
subdivide :: (RealFloat a, Unbox a)
=> a -> a -> Int -> V2 a -> V2 a -> V2 a -> Vector (V2 a)
subdivide mDistanceToleranceSquare mAngleTolerance level
v1@(V2 x1 y1) v2@(V2 x2 y2) v3@(V2 x3 y3)
| level > curveRecursionLimit = V.empty
| otherwise =
-- calculate the midpoints of the line segments
let v12 = (v1 + v2) / 2
v23 = (v2 + v3) / 2
v123= (v12 + v23) / 2
V2 dx dy = v3 - v1
d = abs $ (x2 - x3) * dy - (y2 - y3) * dx
subdivideFurther = subdivide mDistanceToleranceSquare
mAngleTolerance
(level + 1)
v1 v12 v123
V.++
subdivide mDistanceToleranceSquare
mAngleTolerance
(level + 1)
v123 v23 v3
in if d > realToFrac curveCollinearityEpsilon
then -- test regular case
if (d * d) <= (mDistanceToleranceSquare * (dx*dx + dy*dy))
then -- if the curvature is within our distance tolerance then
-- we're done subdividing
if mAngleTolerance < realToFrac curveAngleToleranceEpsilon
then V.singleton v123
else -- test angle and cusp condition
let preDA = abs (atan2 (y3 - y2) (x3 - x2) -
atan2 (y2 - y1) (x2 - x1))
da = if preDA >= pi then 2*pi - preDA else preDA
in if da < mAngleTolerance
then V.singleton v123
else subdivideFurther
else subdivideFurther
else -- test collinear case
let da = dx*dx + dy*dy
f a b = if quadrance (a - b) < mDistanceToleranceSquare
then V.singleton v2
else subdivideFurther
in if da == 0
then f v1 v2
else let d' = ((x2 - x1) * dx + (y2 - y1) * dy) / da
in if d' > 0 && d' < 1
then -- this is the simple collinear case, 1-2-3
V.empty
else f v2 $ if d' <= 0
then v1
else if d' >= 1
then v3
else V2 (x1 + d'*dx) (y1 + d'*dy)
--------------------------------------------------------------------------------
-- Shapes with beziers
--------------------------------------------------------------------------------
kappa :: Fractional a => a
kappa = 0.5522847498307936
cornerBez4 :: RealFloat a => a -> a -> CubicBezier (V2 a)
cornerBez4 xr yr = bez4 (V2 0 yr) (V2 0 y) (V2 x 0) (V2 xr 0)
where x = xr * kappa
y = yr * kappa
cornerBez3 :: (RealFloat a, Unbox a)
=> a -> a -> Vector (QuadraticBezier (V2 a))
cornerBez3 xr yr = demoteCubic $ cornerBez4 xr yr
-- | Generate a cubic Bezier representing an arc on the unit circle of total
-- angle `size` radians, beginning `start` radians above the x-axis. Up to four
-- of these curves are combined to make a full arc.
-- See www.joecridge.me/bezier.pdf for an explanation of the method.
acuteArc :: RealFloat a => a -> a -> CubicBezier (V2 a)
acuteArc start size = bez4 a b c d
where [a,b,c,d] = [V2 ax ay, V2 bx by, V2 cx cy, V2 dx dy]
ax = cos start
ay = sin start
bx = lambda * cosPhi + mu * sinPhi
by = lambda * sinPhi - mu * cosPhi
cx = lambda * cosPhi - mu * sinPhi
cy = lambda * sinPhi + mu * cosPhi
dx = cos (start + size)
dy = sin (start + size)
alpha = size / 2
cosAlpha = cos alpha
sinAlpha = sin alpha
cotAlpha = 1 / tan alpha
phi = start + alpha
cosPhi = cos phi
sinPhi = sin phi
lambda = (4 - cosAlpha) / 3
mu = sinAlpha + (cosAlpha - lambda) * cotAlpha
curveEpsilon :: Fractional a => a
curveEpsilon = 0.00001
-- | Create a list of cubic beziers representing an arc along an ellipse with
-- width `w`, height `h` and total angle `stop - start` radians, beginning
-- `start` radians above the x-axis.
arcBez4,arc' :: (Epsilon a, RealFloat a)
=> a -> a -> a -> a -> [CubicBezier (V2 a)]
arcBez4 w h start stop = if stop - start >= 2*pi
then close $ arc' w h start (start + 2*pi)
else arc' w h start stop
-- This is a full arc so make sure the first and last points are equal
where close [c1@(d,_,_,_) ,c2,c3,(a,b,c,_)] =
[c1,c2,c3,(a,b,c,d)]
close cs = cs
arc' w h start stop
| (stop - start) > curveEpsilon = a : arcBez4 w h (start + arcToDraw) stop
| otherwise = []
where arcToDraw = min (stop - start) (pi/2)
mv = affine2Modelview $ Scale (realToFrac <$> V2 w h)
a = transformCubicBezier mv $ acuteArc start arcToDraw
arcBez3 :: (Epsilon a, RealFloat a, Unbox a)
=> a -> a -> a -> a -> Vector (QuadraticBezier (V2 a))
arcBez3 w h start stop = V.concat $ map demoteCubic $ arcBez4 w h start stop
-- | Create a list of cubic beziers that represent an entire closed
-- ellipse.
ellipseBez4 :: (Epsilon a, RealFloat a) => a -> a -> [CubicBezier (V2 a)]
ellipseBez4 xr yr = arcBez4 xr yr 0 (2*pi)
ellipseBez3 :: (Epsilon a, RealFloat a, Unbox a)
=> a -> a -> Vector (QuadraticBezier (V2 a))
ellipseBez3 xr yr = V.concat $ map demoteCubic $ ellipseBez4 xr yr