packages feed

moonlight-planar-1.1.0.0: src-illustration/Moonlight/Planar/Illustration.hs

{-# LANGUAGE DeriveFunctor #-}

-- | Ordered painting of canonical curves. Parts remain ordinary author-owned
-- values; annotations neither resolve references nor change geometry.
module Moonlight.Planar.Illustration
  ( Picture
  , Color (..)
  , Paint (..)
  , GradientStop (..)
  , GradientStops
  , gradientStops
  , gradientStopValues
  , FillRule (..)
  , StrokeUnits (..)
  , LineCap (..)
  , LineJoin (..)
  , MiterLimit
  , miterLimit
  , miterLimitValue
  , miterLimitFour
  , IllustrationError (..)
  , StrokeStyle (..)
  , fill
  , stroke
  , clip
  , place
  , opacity
  , annotate
  , PictureAlgebra (..)
  , foldPicture
  ) where

import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Word (Word8)
import Moonlight.Planar.Affine (Affine2)
import Moonlight.Planar.Curve (ClosedTrail, Located, Path)
import Moonlight.Planar.Exact (ExactPoint, ExactRational, PositiveExact, UnitInterval)

data Color = RGB !Word8 !Word8 !Word8
  deriving stock (Eq, Ord, Show)

-- | Gradient positions use the same local coordinates as their painted path.
-- Opacity is explicit, rather than encoded in a color string.
data GradientStop = GradientStop !UnitInterval !Color !UnitInterval
  deriving stock (Eq, Show)

newtype GradientStops = GradientStops (NonEmpty GradientStop)
  deriving stock (Eq, Show)

-- | One-time authoring canonicalization: increasing offsets, with stable
-- author order at coincident offsets (which express a discontinuous edge).
-- Browser-specific clamping of descending offsets is never needed.
gradientStops :: NonEmpty GradientStop -> GradientStops
gradientStops = GradientStops . NonEmpty.sortWith (\(GradientStop offset _ _) -> offset)

gradientStopValues :: GradientStops -> NonEmpty GradientStop
gradientStopValues (GradientStops stops) = stops

data Paint
  = Solid !Color
  | LinearGradient !ExactPoint !ExactPoint !GradientStops
  | RadialGradient !ExactPoint !PositiveExact !GradientStops
  deriving stock (Eq, Show)

data FillRule = NonZero | EvenOdd
  deriving stock (Eq, Ord, Show)

data StrokeUnits = LocalUnits | OutputUnits
  deriving stock (Eq, Ord, Show)

data LineCap = ButtCap | RoundCap | SquareCap
  deriving stock (Eq, Ord, Show)

newtype MiterLimit = MiterLimit ExactRational
  deriving stock (Eq, Show)

data IllustrationError = InvalidMiterLimit !ExactRational
  deriving stock (Eq, Show)

-- | SVG's miter ratio has a lower bound of one, not merely positivity.
miterLimit :: ExactRational -> Either IllustrationError MiterLimit
miterLimit value
  | value < 1 = Left (InvalidMiterLimit value)
  | otherwise = Right (MiterLimit value)

miterLimitValue :: MiterLimit -> ExactRational
miterLimitValue (MiterLimit value) = value

miterLimitFour :: MiterLimit
miterLimitFour = MiterLimit 4

data LineJoin = MiterJoin !MiterLimit | RoundJoin | BevelJoin
  deriving stock (Eq, Show)

data StrokeStyle = StrokeStyle
  { strokePaint :: !Paint
  , strokeWidth :: !PositiveExact
  , strokeUnits :: !StrokeUnits
  , strokeCap :: !LineCap
  , strokeJoin :: !LineJoin
  }
  deriving stock (Eq, Show)

-- The sequence, rather than a binary composition tree, makes the monoid's
-- associativity structural and gives publication stable sibling addresses.
newtype Picture part = Picture (Seq (Mark part))
  deriving stock (Eq, Show, Functor)

data Mark part
  = Filled !FillRule !Paint !(NonEmpty (Located ClosedTrail))
  | Stroked !StrokeStyle !Path
  | Clipped !FillRule !(NonEmpty (Located ClosedTrail)) !(Picture part)
  | Placed !Affine2 !(Picture part)
  | Transparent !UnitInterval !(Picture part)
  | Annotated !part !(Picture part)
  deriving stock (Eq, Show, Functor)

instance Semigroup (Picture part) where
  Picture behind <> Picture inFront = Picture (behind <> inFront)

instance Monoid (Picture part) where
  mempty = Picture Seq.empty

fill :: FillRule -> Paint -> NonEmpty (Located ClosedTrail) -> Picture part
fill rule paint contours = Picture (Seq.singleton (Filled rule paint contours))

stroke :: StrokeStyle -> Path -> Picture part
stroke style curve = Picture (Seq.singleton (Stroked style curve))

clip :: FillRule -> NonEmpty (Located ClosedTrail) -> Picture part -> Picture part
clip rule contours picture = Picture (Seq.singleton (Clipped rule contours picture))

place :: Affine2 -> Picture part -> Picture part
place affine picture = Picture (Seq.singleton (Placed affine picture))

-- | Opacity applies once to the composite, not independently to its children.
opacity :: UnitInterval -> Picture part -> Picture part
opacity amount picture = Picture (Seq.singleton (Transparent amount picture))

annotate :: part -> Picture part -> Picture part
annotate part picture = Picture (Seq.singleton (Annotated part picture))

-- | The interpreter boundary exposes painting operations, never SVG syntax or
-- a second geometry AST. A carrier may be a pure environment-reading function.
data PictureAlgebra part a = PictureAlgebra
  { paintSequence :: Seq a -> a
  , paintFill :: FillRule -> Paint -> NonEmpty (Located ClosedTrail) -> a
  , paintStroke :: StrokeStyle -> Path -> a
  , paintClip :: FillRule -> NonEmpty (Located ClosedTrail) -> a -> a
  , paintPlace :: Affine2 -> a -> a
  , paintOpacity :: UnitInterval -> a -> a
  , paintAnnotation :: part -> a -> a
  }

foldPicture :: PictureAlgebra part a -> Picture part -> a
foldPicture algebra = fold
 where
  fold (Picture marks) = paintSequence algebra (interpret <$> marks)
  interpret mark = case mark of
    Filled rule paint contours -> paintFill algebra rule paint contours
    Stroked style curve -> paintStroke algebra style curve
    Clipped rule contours picture -> paintClip algebra rule contours (fold picture)
    Placed affine picture -> paintPlace algebra affine (fold picture)
    Transparent amount picture -> paintOpacity algebra amount (fold picture)
    Annotated part picture -> paintAnnotation algebra part (fold picture)