packages feed

brush-strokes-0.1.0.0: src/lib/Calligraphy/Brushes.hs

{-# LANGUAGE AllowAmbiguousTypes  #-}
{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE PolyKinds            #-}
{-# LANGUAGE RebindableSyntax     #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE UndecidableInstances #-}

module Calligraphy.Brushes
  ( BrushFn, Brush(..), Corner(..)
  , Unrotated(..)
  , circleBrush
  , ellipseBrush
  , tearDropBrush, tearDropHeightOffset
  , roundTearDropBrush
  ) where

-- base
import Prelude
  hiding
    ( Num(..), Floating(..), (^), (/), fromInteger, fromRational )
import Data.Kind
  ( Type, Constraint )
import GHC.Exts
  ( Proxy#, proxy# )
import GHC.TypeNats
  ( Nat )

-- acts
import Data.Act
  ( Torsor((-->)) )

-- containers
import Data.Sequence
  ( Seq )
import qualified Data.Sequence as Seq
  ( empty, fromList, singleton )

-- brush-strokes
import Math.Algebra.Dual
import Math.Bezier.Spline
import Math.Differentiable
  ( I, IVness(..), SingIVness (..) )
import Math.Interval
  ( 𝕀ℝ, singleton, point )
import Math.Linear
import Math.Module
import Math.Ring

--------------------------------------------------------------------------------

-- | The shape of a brush (before applying any rotation).
type BrushFn :: IVness -> Nat -> Nat -> Type
type BrushFn i k nbBrushParams =
  Unrotated
    ( C k ( I i nbBrushParams ) ( Spline Closed () ( I i 2 ) ) )

-- | A newtype wrapper to enforce applying rotation.
newtype Unrotated a = Unrotated { getUnrotated :: a }
  deriving stock ( Eq, Ord, Show, Functor, Foldable, Traversable )

-- | A corner of a brush.
type Corner :: Type -> Type
data Corner a
  = Corner
  { cornerPoint :: a
  , cornerStartTangent, cornerEndTangent :: T a
  }
  deriving stock ( Eq, Show, Functor, Foldable, Traversable )

instance Applicative Corner where
  pure a = Corner a ( T a ) ( T a )
  Corner f ( T g ) ( T h ) <*> Corner a ( T b ) ( T c ) =
    Corner ( f a ) ( T $ g b ) ( T $ h c )

instance forall r a. ( Ring r, Module r ( T a ) ) => Module r ( T ( Corner a ) ) where
  origin = T $ pure $ unT $ origin @r @( T a )
  ( T f ) ^+^ ( T g ) = T $ liftA2 ( \ x y -> unT $ T x ^+^ T y ) f g
  ( T f ) ^-^ ( T g ) = T $ liftA2 ( \ x y -> unT $ T x ^+^ T y ) f g
  a *^ T f = T $ fmap ( \ x -> unT $ a *^ T x ) f

-- | A brush, described as a base shape + an optional rotation.
data Brush nbBrushParams
  = Brush
  { -- | Base brush shape, before applying rotation (if any).
    brushBaseShape  :: BrushFn NonIV 2 nbBrushParams
  , -- | Base brush shape, before applying rotation (if any).
    brushBaseShape3 :: BrushFn NonIV 3 nbBrushParams
    -- | Base brush shape, before applying rotation (if any).
  , brushBaseShapeI :: BrushFn IsIV  3 nbBrushParams

    -- | Brush corners, before applying rotation (if any).
  , brushCorners   :: Seq ( Unrotated ( C 2 ( ℝ nbBrushParams ) ( Corner ( I NonIV 2 ) ) ) )
    -- | Brush corners, before applying rotation (if any).
  , brushCorners3  :: Seq ( Unrotated ( C 3 ( ℝ nbBrushParams ) ( Corner ( I NonIV 2 ) ) ) )
    -- | Brush corners, before applying rotation (if any).
  , brushCornersI  :: Seq ( Unrotated ( C 3 ( 𝕀ℝ nbBrushParams ) ( Corner ( I IsIV 2 ) ) ) )

    -- | Optional rotation angle function
    -- (assumed to be a linear function).
  , mbRotation     :: Maybe ( ℝ nbBrushParams -> Double )
  }

--------------------------------------------------------------------------------
-- Brushes

-- Some convenience type synonyms for brush types... a bit horrible
type ParamsICt :: Nat -> IVness -> Nat -> Constraint
type ParamsICt k i nbParams =
    ( Module
       ( D k nbParams ( I i Double ) )
       ( D k nbParams ( I i 2 ) )
    , Module ( I i Double ) ( T ( I i Double ) )
    , HasChainRule ( I i Double ) k ( I i nbParams )
    , Representable ( I i Double ) ( I i nbParams )
    , Applicative ( D k nbParams )
    , Dim ( I i nbParams ) ~ nbParams
    )

{-# INLINEABLE circleBrush #-}
circleBrush :: Brush 1
circleBrush =
  Brush
    { brushBaseShape  = Unrotated $ circleBrushFn @2 SNonIV
    , brushBaseShape3 = Unrotated $ circleBrushFn @3 SNonIV
    , brushBaseShapeI = Unrotated $ circleBrushFn @3 SIsIV
    , brushCorners    = Seq.empty
    , brushCorners3   = Seq.empty
    , brushCornersI   = Seq.empty
    , mbRotation      = Nothing
    }

{-# INLINEABLE ellipseBrush #-}
ellipseBrush :: Brush 3
ellipseBrush =
  Brush
    { brushBaseShape  = Unrotated $ ellipseBrushFn @2 SNonIV
    , brushBaseShape3 = Unrotated $ ellipseBrushFn @3 SNonIV
    , brushBaseShapeI = Unrotated $ ellipseBrushFn @3 SIsIV
    , brushCorners    = Seq.empty
    , brushCorners3   = Seq.empty
    , brushCornersI   = Seq.empty
    , mbRotation      = Just ( `index` ( Fin 3 ) )
    }

{-# INLINEABLE tearDropBrush #-}
tearDropBrush :: Brush 3
tearDropBrush =
  Brush
    { brushBaseShape  = Unrotated $ tearDropBrushFn @2 SNonIV
    , brushBaseShape3 = Unrotated $ tearDropBrushFn @3 SNonIV
    , brushBaseShapeI = Unrotated $ tearDropBrushFn @3 SIsIV
    , brushCorners    = Seq.singleton $ Unrotated $ tearDropCorner @2 proxy# SNonIV
    , brushCorners3   = Seq.singleton $ Unrotated $ tearDropCorner @3 proxy# SNonIV
    , brushCornersI   = Seq.singleton $ Unrotated $ tearDropCorner @3 proxy# SIsIV
    , mbRotation      = Just ( `index` ( Fin 3 ) )
    }

tearDropHeightOffset :: Double
tearDropHeightOffset = 2 / 14

--------------------------------------------------------------------------------

type SplineFn k i nbParams
  =  SingIVness i
  -> C k ( I i nbParams ) ( Spline 'Closed () ( I i 2 ) )

type CornerFn k i nbParams
  =  Proxy# k
  -> SingIVness i
  -> C k ( I i nbParams ) ( Corner ( I i 2 ) )

--------------------------------------------------------------------------------
-- Circle & ellipse

-- | Root of @(Sqrt[2] (4 + 3 κ) - 16) (2 - 3 κ)^2 - 8 (1 - 3 κ) Sqrt[8 - 24 κ + 12 κ^2 + 8 κ^3 + 3 κ^4]@.
--
-- Used to approximate circles and ellipses with Bézier curves.
κ :: Double
κ = 0.5519150244935105707435627227925

circleSpline :: forall d v
             .  Applicative d
             => ( Double -> Double -> d v )
             -> d ( Spline 'Closed () v )
circleSpline p = sequenceA $
  Spline { splineStart  = p 1 0
         , splineCurves = ClosedCurves crvs lastCrv }
  where
    crvs = Seq.fromList
      [ Bezier3To ( p  1  κ ) ( p  κ  1 ) ( NextPoint ( p  0  1 ) ) ()
      , Bezier3To ( p -κ  1 ) ( p -1  κ ) ( NextPoint ( p -1  0 ) ) ()
      , Bezier3To ( p -1 -κ ) ( p -κ -1 ) ( NextPoint ( p  0 -1 ) ) ()
      ]
    lastCrv =
        Bezier3To ( p  κ -1 ) ( p  1 -κ ) BackToStart ()
{-# INLINE circleSpline #-}

mkI1 :: SingIVness i -> Double -> I i Double
mkI1 = \case
  SNonIV -> id
  SIsIV  -> singleton
{-# INLINE mkI1 #-}

mkI2 :: SingIVness i -> ℝ 2 -> I i 2
mkI2 = \case
  SNonIV -> id
  SIsIV  -> point
{-# INLINE mkI2 #-}

circleBrushFn :: forall k i nbParams
              .  ( nbParams ~ 1
                 , ParamsICt k i nbParams
                 )
              => SplineFn k i nbParams
circleBrushFn ivness =
  D \ params ->
    let r :: D k nbParams ( I i Double )
        r = runD ( var @_ @k $ Fin 1 ) params
        mkPt :: Double -> Double -> D k nbParams ( I i 2 )
        mkPt x y
          =   ( r `scaledBy` x ) *^ e_x
          ^+^ ( r `scaledBy` y ) *^ e_y
    in circleSpline mkPt
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE circleBrushFn @2 @NonIV #-}
{-# SPECIALISE circleBrushFn @3 @IsIV  #-}

ellipseBrushFn :: forall k i nbParams
               .  ( nbParams ~ 3
                  , ParamsICt k i nbParams
                  )
               => SplineFn k i nbParams
ellipseBrushFn ivness =
  D \ params ->
    let a, b :: D k nbParams ( I i Double )
        a = runD ( var @_ @k $ Fin 1 ) params
        b = runD ( var @_ @k $ Fin 2 ) params
        mkPt :: Double -> Double -> D k nbParams ( I i 2 )
        mkPt x y
          = let !x' = a `scaledBy` x
                !y' = b `scaledBy` y
            in x' *^ e_x ^+^ y' *^ e_y
    in circleSpline mkPt
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE ellipseBrushFn @2 @NonIV #-}
{-# SPECIALISE ellipseBrushFn @3 @IsIV  #-}

--------------------------------------------------------------------------------
-- Tear drop

-- | y-coordinate of the center of mass of the cubic Bézier teardrop
-- with control points at (0,0), (±0.5,√3/2).
tearCenter :: Double
tearCenter = 3 * sqrt 3 / 14

-- | Width of the cubic Bézier teardrop with control points at (0,0), (±0.5,√3/2).
tearWidth :: Double
tearWidth = 1 / ( 2 * sqrt 3 )

-- | Height of the cubic Bézier teardrop with control points at (0,0), (±0.5,√3/2).
tearHeight :: Double
tearHeight = 3 * sqrt 3 / 8

-- √3/2
sqrt3_over_2 :: Double
sqrt3_over_2 = 0.5 * sqrt 3

tearDropBrushFn :: forall k i nbParams
                .  ( nbParams ~ 3
                   , ParamsICt k i nbParams
                   )
                => SplineFn k i nbParams
tearDropBrushFn ivness =
  D \ params ->
    let w, h :: D k nbParams ( I i Double )
        w = 2 * runD ( var @_ @k ( Fin 1 ) ) params
        h = 2 * runD ( var @_ @k ( Fin 2 ) ) params
        mkPt :: Double -> Double -> D k nbParams ( I i 2 )
        mkPt x y
          -- 1. translate the teardrop so that the centre of mass is at the origin
          -- 2. scale the teardrop so that it has the requested width/height
          -- 3. rotate (done separately)
          = let !x' = w `scaledBy` ( x / tearWidth )
                !y' = h `scaledBy` ( ( y - tearCenter ) / tearHeight )
            in x' *^ e_x ^+^ y' *^ e_y

    in sequenceA $
         Spline { splineStart  = mkPt 0 0
                , splineCurves = ClosedCurves Seq.empty $
                  Bezier3To
                    ( mkPt  0.5 sqrt3_over_2 )
                    ( mkPt -0.5 sqrt3_over_2 )
                    BackToStart () }
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE tearDropBrushFn @2 @NonIV #-}
{-# SPECIALISE tearDropBrushFn @2 @IsIV  #-}

tearDropCorner :: forall k i nbParams. ( nbParams ~ 3, ParamsICt k i nbParams, Torsor ( T ( I i 2 ) ) ( I i 2 ) ) => CornerFn k i nbParams
tearDropCorner _ ivness =
  D \ params ->
    let w, h :: D k nbParams ( I i Double )
        w = 2 * runD ( var @_ @k ( Fin 1 ) ) params
        h = 2 * runD ( var @_ @k ( Fin 2 ) ) params
        mkPt :: Double -> Double -> D k nbParams ( I i 2 )
        mkPt x y
          -- 1. translate the teardrop so that the centre of mass is at the origin
          -- 2. scale the teardrop so that it has the requested width/height
          -- 3. rotate (done separately)
          = let !x' = w `scaledBy` ( x / tearWidth )
                !y' = h `scaledBy` ( ( y - tearCenter ) / tearHeight )
            in x' *^ e_x ^+^ y' *^ e_y
        !cornerPoint = mkPt 0 0
    in sequenceA $
         Corner
          { cornerPoint
          , cornerStartTangent = T $ liftA2 ( \ x y -> unT $ x --> y ) ( mkPt -0.5 sqrt3_over_2 ) cornerPoint
          , cornerEndTangent   = T $ liftA2 ( \ x y -> unT $ x --> y ) cornerPoint ( mkPt 0.5 sqrt3_over_2 )
          }
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE tearDropCorner @2 @NonIV #-}
{-# SPECIALISE tearDropCorner @3 @IsIV  #-}

--------------------------------------------------------------------------------
-- Curl Drop

{-# INLINEABLE roundTearDropBrush #-}
roundTearDropBrush :: Brush 3
roundTearDropBrush =
  Brush
    { brushBaseShape  = Unrotated $ roundTearDropBrushFn @2 SNonIV
    , brushBaseShape3 = Unrotated $ roundTearDropBrushFn @3 SNonIV
    , brushBaseShapeI = Unrotated $ roundTearDropBrushFn @3 SIsIV
    , brushCorners    = Seq.fromList
        [ Unrotated $ roundTearDropCorner @2 proxy# SNonIV
        , Unrotated $ roundTearDropSideCorner @2 proxy# SNonIV True
        , Unrotated $ roundTearDropSideCorner @2 proxy# SNonIV False
        ]
    , brushCorners3   = Seq.fromList
        [ Unrotated $ roundTearDropCorner @3 proxy# SNonIV
        , Unrotated $ roundTearDropSideCorner @3 proxy# SNonIV True
        , Unrotated $ roundTearDropSideCorner @3 proxy# SNonIV False
        ]
    , brushCornersI   = Seq.fromList
        [ Unrotated $ roundTearDropCorner @3 proxy# SIsIV
        , Unrotated $ roundTearDropSideCorner @3 proxy# SIsIV True
        , Unrotated $ roundTearDropSideCorner @3 proxy# SIsIV False
        ]
    , mbRotation      = Just ( `index` ( Fin 3 ) )
    }

roundTearDropBrushFn :: forall k i nbParams
                .  ( nbParams ~ 3
                   , ParamsICt k i nbParams
                   )
                => SplineFn k i nbParams
roundTearDropBrushFn ivness =
  D \ params ->
    let h, r :: D k nbParams ( I i Double )
        h = 2 * runD ( var @_ @k ( Fin 1 ) ) params
        r =     runD ( var @_ @k ( Fin 2 ) ) params

        mkPt :: D k nbParams ( I i Double ) -> D k nbParams ( I i Double ) -> D k nbParams ( I i 2 )
        mkPt x y = ( x - h `scaledBy` 0.5 ) *^ e_x ^+^ y *^ e_y

        curves = Seq.fromList
          [
            Bezier2To
              ( mkPt ( r `scaledBy` 0.5 ) -r )
              ( NextPoint ( mkPt ( h - r ) -r ) )
              ()

          , Bezier3To
              ( mkPt ( h + r `scaledBy` ( κ - 1 ) ) -r )
              ( mkPt h ( -r `scaledBy` κ ) )
              ( NextPoint ( mkPt h 0 ) )
              ()

          , Bezier3To
              ( mkPt h ( r `scaledBy` κ ) )
              ( mkPt ( h + r `scaledBy` ( κ - 1 ) ) r )
              ( NextPoint ( mkPt ( h - r ) r ) )
              ()
          ]

        lastCrv =
          Bezier2To
            ( mkPt ( r `scaledBy` 0.5 ) r )
            BackToStart
            ()

    in sequenceA $
         Spline { splineStart  = mkPt 0 0
                , splineCurves = ClosedCurves curves lastCrv
                }
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE roundTearDropBrushFn @2 @NonIV #-}
{-# SPECIALISE roundTearDropBrushFn @2 @IsIV  #-}

-- | Main corner of the round tear-drop.
roundTearDropCorner :: forall k i nbParams. ( nbParams ~ 3, ParamsICt k i nbParams, Torsor ( T ( I i 2 ) ) ( I i 2 ) ) => CornerFn k i nbParams
roundTearDropCorner _ ivness =
  D \ params ->
    let h, r :: D k nbParams ( I i Double )
        h = 2 * runD ( var @_ @k ( Fin 1 ) ) params
        r =     runD ( var @_ @k ( Fin 2 ) ) params

        mkPt :: D k nbParams ( I i Double ) -> D k nbParams ( I i Double ) -> D k nbParams ( I i 2 )
        mkPt x y = ( x - h `scaledBy` 0.5 ) *^ e_x ^+^ y *^ e_y

        -- Corner point at the origin
        cornerPoint = mkPt 0 0

        -- First and last control points as defined in roundTearDropBrushFn
        firstControlPoint = mkPt ( r `scaledBy` 0.5 ) -r
        lastControlPoint  = mkPt ( r `scaledBy` 0.5 )  r

    in sequenceA $
         Corner
          { cornerPoint
          -- Define tangent directions based on the control points
          -- Start tangent: from last control point to corner
          , cornerStartTangent = T $ liftA2 ( \ x y -> unT $ x --> y ) lastControlPoint cornerPoint
          -- End tangent: from corner to first control point
          , cornerEndTangent   = T $ liftA2 ( \ x y -> unT $ x --> y ) cornerPoint firstControlPoint
          }
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE roundTearDropCorner @2 @NonIV #-}
{-# SPECIALISE roundTearDropCorner @3 @IsIV  #-}

-- | Corner at the side connection points (h - r, ±r).
-- Takes a Boolean parameter to determine if it's the top (True) or bottom (False) corner.
--
-- Weirdly, this isn't really a corner (as it's flat), but because the derivative
-- is not continuous it can cause cusps in the envelope, so we need to
-- treat it as a corner.
roundTearDropSideCorner :: forall k i nbParams.
                         ( nbParams ~ 3
                         , ParamsICt k i nbParams
                         , Torsor ( T ( I i 2 ) ) ( I i 2 )
                         )
                         => Proxy# k
                         -> SingIVness i
                         -> Bool  -- ^ True for top corner, False for bottom corner
                         -> C k ( I i nbParams ) ( Corner ( I i 2 ) )
roundTearDropSideCorner _ ivness isTop =
  D \ params ->
    let h, r :: D k nbParams ( I i Double )
        h = 2 * runD ( var @_ @k ( Fin 1 ) ) params
        r =     runD ( var @_ @k ( Fin 2 ) ) params

        mkPt :: D k nbParams ( I i Double ) -> D k nbParams ( I i Double ) -> D k nbParams ( I i 2 )
        mkPt x y = ( x - h `scaledBy` 0.5 ) *^ e_x ^+^ y *^ e_y

        -- Sign factor based on whether it's top or bottom
        signFactor = if isTop then 1 else -1

        -- Corner point at (h - r, ±r)
        cornerPoint = mkPt (h - r) (r `scaledBy` signFactor)

        -- Control points for the adjacent curves
        -- For top: prevControlPoint is from cubic, nextControlPoint is from quadratic
        -- For bottom: prevControlPoint is from quadratic, nextControlPoint is from cubic
        (prevControlPoint, nextControlPoint) =
          if isTop
            then
              ( mkPt (h + r `scaledBy` (κ - 1)) (r `scaledBy` signFactor)  -- From cubic Bézier
              , mkPt (r `scaledBy` 0.5) (r `scaledBy` signFactor)          -- From quadratic Bézier
              )
            else
              ( mkPt (r `scaledBy` 0.5) (r `scaledBy` signFactor)          -- From quadratic Bézier
              , mkPt (h + r `scaledBy` (κ - 1)) (r `scaledBy` signFactor)  -- From cubic Bézier
              )

    in sequenceA $
         Corner
          { cornerPoint
          , cornerStartTangent = T $ liftA2 ( \ x y -> unT $ x --> y ) prevControlPoint cornerPoint
          , cornerEndTangent   = T $ liftA2 ( \ x y -> unT $ x --> y ) cornerPoint nextControlPoint
          }
  where
    e_x, e_y :: D k nbParams ( I i 2 )
    e_x = pure $ mkI2 ivness $ ℝ2 1 0
    e_y = pure $ mkI2 ivness $ ℝ2 0 1

    scaledBy :: D k nbParams ( I i Double ) -> Double -> D k nbParams ( I i Double )
    scaledBy d x = fmap ( mkI1 ivness x * ) d
{-# SPECIALISE roundTearDropSideCorner @2 @NonIV #-}
{-# SPECIALISE roundTearDropSideCorner @3 @IsIV  #-}