packages feed

brush-strokes-0.1.0.0: src/lib/Math/Bezier/Quadratic.hs

{-# LANGUAGE AllowAmbiguousTypes  #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE UndecidableInstances #-}

module Math.Bezier.Quadratic
  ( Bezier(..)
  , bezier, bezier', bezier''
  , curvature, squaredCurvature, signedCurvature
  , subdivide, restrict
  , ddist, closestPoint
  , interpolate
  , extrema
  , speedCriticalPoints
  )
  where

-- base
import Data.List.NonEmpty
  ( NonEmpty(..) )
import Data.Monoid
  ( Ap(..) )
import Data.Semigroup
  ( ArgMin, Min(..), Arg(..) )
import GHC.Generics
  ( Generic, Generic1
  , Generically(..), Generically1(..)
  )

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

-- deepseq
import Control.DeepSeq
  ( NFData, NFData1 )

-- groups
import Data.Group
  ( Group )

-- groups-generic
import Data.Group.Generics
  ()

-- primitive
import Data.Primitive.Types
  ( Prim )

-- brush-strokes
import Math.Epsilon
  ( epsilon )
import Math.Module
  ( Module (..)
  , lerp
  , Inner(..), norm, squaredNorm
  , Cross((×))
  )
import Math.Roots
  ( realRoots )
import Math.Linear
  ( ℝ(..), T(..) )
import qualified Math.Ring as Ring

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

-- | Points defining a quadratic Bézier curve (Bernstein form).
--
-- @ p0 @ and @ p2 @ are endpoints, whereas @ p1 @ is a control point.
data Bezier p
  = Bezier
    { p0, p1, p2 :: !p }
  deriving stock ( Generic, Generic1, Functor, Foldable, Traversable )
  deriving ( Semigroup, Monoid, Group )
    via Generically ( Bezier p )
  deriving Applicative
    via Generically1 Bezier
  deriving anyclass ( NFData, NFData1 )

deriving via Ap Bezier p
  instance {-# OVERLAPPING #-} Act v p => Act v ( Bezier p )
deriving via Ap Bezier ( T b )
  instance Module r ( T b ) => Module r ( T ( Bezier b ) )

instance Show p => Show (Bezier p) where
  show (Bezier p1 p2 p3) =
    show p1 ++ "--" ++ show p2 ++ "->" ++ show p3

-- | Quadratic Bézier curve.
bezier :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> p
bezier ( Bezier {..} ) t = lerp @v t ( lerp @v t p0 p1 ) ( lerp @v t p1 p2 )
{-# INLINEABLE bezier #-}

-- | Derivative of a quadratic Bézier curve.
bezier' :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> v
bezier' ( Bezier {..} ) t = Ring.fromInteger 2 *^ lerp @v t ( p0 --> p1 ) ( p1 --> p2 )
{-# INLINEABLE bezier' #-}

-- | Second derivative of a quadratic Bézier curve.
bezier'' :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> v
bezier'' ( Bezier {..} ) = Ring.fromInteger 2 *^ ( p1 --> p0 ^+^ p1 --> p2 )
{-# INLINEABLE bezier'' #-}

-- | Curvature of a quadratic Bézier curve.
curvature :: forall v r p. ( Torsor v p, Inner r v, RealFloat r ) => Bezier p -> r -> r
curvature bez t = sqrt $ squaredCurvature @v bez t
{-# INLINEABLE curvature #-}

-- | Square of curvature of a quadratic Bézier curve.
squaredCurvature :: forall v r p. ( Torsor v p, Inner r v, RealFloat r ) => Bezier p -> r -> r
squaredCurvature bez t
  | sq_nm_g' < epsilon
  = 1 / 0
  | otherwise
  = ( sq_nm_g' * squaredNorm @v g'' - ( g' ^.^ g'' ) ^ ( 2 :: Int ) )
  / ( sq_nm_g' ^ ( 3 :: Int ) )
  where
    g', g'' :: v
    !g'  = bezier'  @v bez t
    !g'' = bezier'' @v bez
    sq_nm_g' :: r
    !sq_nm_g' = squaredNorm @v g'
{-# INLINEABLE squaredCurvature #-}

-- | Signed curvature of a planar quadratic Bézier curve.
signedCurvature :: Bezier ( ℝ 2 ) -> Double -> Double
signedCurvature bez t = ( g' × g'' ) / norm g' ^ ( 3 :: Int )
  where
    g', g'' :: T ( ℝ 2 )
    !g'  = bezier'  @( T ( ℝ 2 ) ) bez t
    !g'' = bezier'' @( T ( ℝ 2 ) ) bez

-- | Subdivide a quadratic Bézier curve into two parts.
subdivide :: forall v r p. ( Torsor v p, Module r v ) => Bezier p -> r -> ( Bezier p, Bezier p )
subdivide ( Bezier {..} ) t = ( Bezier p0 q1 pt, Bezier pt r1 p2 )
  where
    pt, q1, r1 :: p
    !q1 = lerp @v t p0 p1
    !r1 = lerp @v t p1 p2
    !pt = lerp @v t q1 r1
{-# INLINEABLE subdivide #-}

-- | Restrict a quadratic Bézier curve to a sub-interval, re-parametrising
-- to \( [0,1] \).
restrict :: forall v r p. ( Torsor v p, Ring.Field r, Module r v ) => Bezier p -> ( r , r ) -> Bezier p
restrict bez ( a, b ) = fst $ ( flip ( subdivide @v ) b' ) $ snd $ subdivide @v bez a
  where
    !b' = ( b Ring.- a ) Ring./ ( Ring.fromInteger 1 Ring.- a )
  -- TODO: this could be made more efficient.
  -- See e.g. "https://math.stackexchange.com/questions/4172835/cubic-b%C3%A9zier-spline-multiple-split"
  -- or the paper "On the numerical condition of Bernstein-Bézier subdivision process".
{-# INLINEABLE restrict #-}

-- | Polynomial coefficients of the derivative of the distance to a quadratic Bézier curve.
ddist :: forall v r p. ( Torsor v p, Inner r v, RealFloat r ) => Bezier p -> p -> [ r ]
ddist ( Bezier {..} ) c = [ a3, a2, a1, a0 ]
  where
    v, v', v'' :: v
    !v   = c  --> p0
    !v'  = p0 --> p1
    !v'' = p1 --> p0 ^+^ p1 --> p2

    a0, a1, a2, a3 :: r
    !a0 = v ^.^ v'
    !a1 = v ^.^ v'' + 2 * squaredNorm v'
    !a2 = 3 * v' ^.^ v''
    !a3 = squaredNorm v''
{-# INLINEABLE ddist #-}

-- | Finds the closest point to a given point on a quadratic Bézier curve.
closestPoint
  :: forall v r p. ( Torsor v p, Inner r v, RealFloat r, Prim r, NFData r )
  => Bezier p -> p -> ArgMin r ( r, p )
closestPoint pts c = pickClosest ( 0 :| 1 : roots )
  where
    roots :: [ r ]
    roots = filter ( \ r -> r > 0 && r < 1 ) ( realRoots 50 $ ddist @v pts c )

    pickClosest :: NonEmpty r -> ArgMin r ( r, p )
    pickClosest ( s :| ss ) = go s q nm0 ss
      where
        q :: p
        !q  = bezier @v pts s
        nm0 :: r
        !nm0 = squaredNorm ( c --> q :: v )
        go t p nm  [] = Min ( Arg nm ( t, p ) )
        go t p nm ( t' : ts )
          | nm' < nm  = go t' p' nm' ts
          | otherwise = go t  p  nm  ts
          where
            p' :: p
            !p' = bezier @v pts t'
            nm' :: r
            !nm' = squaredNorm ( c --> p' :: v )
{-# INLINEABLE closestPoint #-}

-- | Interpolation of a quadratic Bézier control point, given path points and Bézier parameter.
--
-- That is, given points `p0`, `p2`, and `q`, and parameter \( 0 < t < 1 \),
-- this function finds the unique control point `p1` such that
-- the quadratic Bézier curve with parameters `p0`, `p1`, `p2` passes through
-- the point `q` at time `t`.
interpolate :: forall v r p. ( Torsor v p, Module r v, Fractional r ) => p -> p -> r -> p -> Bezier p
interpolate p0 p2 t q = Bezier {..}
  where
    p1 :: p
    !p1 = (   ( 0.5 * ( t - 1 ) / t ) *^ ( q --> p0 :: v )
          ^+^ ( 0.5 * t / ( t - 1 ) ) *^ ( q --> p2 :: v )
          ) • q
{-# INLINEABLE interpolate #-}

-- | Critical points of the speed @|B′(t)|@ in @(0,1)@ for a quadratic Bézier.
--
-- These are the roots of @d\/dt |B′(t)|² = 0@, i.e. where the hodograph
-- @H(t) = d₀ + t·f@ is perpendicular to its (constant) derivative @f = d₁ − d₀@.
speedCriticalPoints
  :: forall diff ptData
  .  ( Torsor diff ptData, Module Double diff, Inner Double diff )
  => Bezier ptData -> [ Double ]
speedCriticalPoints ( Bezier {..} ) =
    let
      d0, d1 :: diff
      !d0 = p0 --> p1
      !d1 = p1 --> p2
      !f  = d1 ^-^ d0
      !ff = f ^.^ f
      !tc = -(f ^.^ d0) / ff
    in [ tc | ff > 0, tc > 0, tc < 1 ]
{-# INLINEABLE speedCriticalPoints #-}

-- | Extremal values of the Bézier parameter for a quadratic Bézier curve.
extrema :: Fractional r => Bezier r -> [ r ]
extrema ( Bezier {..} ) = [ t ]
  where
    !t = ( p0 - p1 ) / ( p0 - 2 * p1 + p2 )
{-# INLINEABLE extrema #-}