packages feed

nano-svg-0.2.0.0: lib/Graphics/NanoSvg/Types.hs

{-# LANGUAGE DerivingStrategies #-}

-- |
-- Module      : Graphics.NanoSvg.Types
-- Copyright   : (c) 2026 goolord
-- License     : MIT
--
-- The flattened document model: shapes in paint order, each with absolute
-- segments, a transform into the viewBox and a resolved style.
module Graphics.NanoSvg.Types
  ( -- * Colors
    RGBA (..)
  , rgba
  , black

    -- * Geometry
  , Point (..)
  , Box (..)
  , Matrix (..)
  , identity
  , multiply
  , translate
  , transformPoint
  , averageScale

    -- * Shapes
  , Segment (..)
  , Paint (..)
  , FillRule (..)
  , LineCap (..)
  , LineJoin (..)
  , Style (..)
  , defaultStyle
  , Shape (..)
  , Document (..)
  )
where

import Data.Primitive.SmallArray (SmallArray)
import Data.Word (Word32, Word8)
import Text.Printf (printf)

-- | A color packed as @0xRRGGBBAA@, not premultiplied. Shown as @#rrggbbaa@.
newtype RGBA = RGBA {rgbaToWord32 :: Word32}
  deriving newtype (Eq, Ord)

instance Show RGBA where
  show (RGBA w) = printf "#%08x" w

-- | Pack red, green, blue and alpha; alpha 255 is opaque.
rgba :: Word8 -> Word8 -> Word8 -> Word8 -> RGBA
rgba r g b a = RGBA (foldl' (\acc c -> acc * 256 + fromIntegral c) 0 [r, g, b, a])

-- | Opaque black, the initial value of @fill@.
black :: RGBA
black = rgba 0 0 0 255

data Point = Point !Float !Float
  deriving stock (Eq, Show)

-- | A @viewBox@.
data Box = Box {boxX, boxY, boxW, boxH :: !Float}
  deriving stock (Eq, Show)

-- | @matrix(a b c d e f)@, mapping @(x, y)@ to @(a*x + c*y + e, b*x + d*y + f)@.
data Matrix = Matrix {matrixA, matrixB, matrixC, matrixD, matrixE, matrixF :: !Float}
  deriving stock (Eq, Show)

identity :: Matrix
identity = Matrix 1 0 0 1 0 0

-- | @multiply outer inner@ applies @inner@ first.
multiply :: Matrix -> Matrix -> Matrix
multiply (Matrix a b c d e f) (Matrix a' b' c' d' e' f') =
  Matrix (a * a' + c * b') (b * a' + d * b') (a * c' + c * d') (b * c' + d * d') (a * e' + c * f' + e) (b * e' + d * f' + f)

transformPoint :: Matrix -> Point -> Point
transformPoint (Matrix a b c d e f) (Point x y) = Point (a * x + c * y + e) (b * x + d * y + f)

-- | Square root of the absolute determinant: a single scale factor for
-- stroke widths, exact for uniform scaling.
averageScale :: Matrix -> Float
averageScale (Matrix a b c d _ _) = sqrt (abs (a * d - b * c))

-- | @translate(x, y)@.
translate :: Float -> Float -> Matrix
translate = Matrix 1 0 0 1

-- | A path command in absolute, shape-local coordinates. A renderer still
-- tracks the current point and subpath start.
data Segment
  = MoveTo !Point
  | LineTo !Point
  | -- | Two control points and the endpoint.
    CubicTo !Point !Point !Point
  | -- | Control point and endpoint.
    QuadTo !Point !Point
  | -- | Radii, x-axis rotation in degrees, large-arc and sweep flags, endpoint.
    ArcTo !Float !Float !Float !Bool !Bool !Point
  | ClosePath
  deriving stock (Eq, Show)

-- | 'PaintCurrent' is @currentColor@; @none@ and @transparent@ are 'PaintNone'.
data Paint = PaintNone | PaintCurrent | PaintColor !RGBA
  deriving stock (Eq, Show)

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

data LineCap = CapButt | CapRound | CapSquare
  deriving stock (Eq, Show)

data LineJoin = JoinMiter | JoinRound | JoinBevel
  deriving stock (Eq, Show)

-- | Resolved presentation properties. An unset ('Nothing') fill is distinct
-- from black so a renderer can tint icons; see 'documentMonochrome'.
data Style = Style
  { styleFill :: !(Maybe Paint)
  , styleStroke :: !(Maybe Paint)
  , styleStrokeWidth :: !Float
  -- ^ Before 'shapeTransform' is applied.
  , styleCap :: !LineCap
  , styleJoin :: !LineJoin
  , styleMiterLimit :: !Float
  , styleFillRule :: !FillRule
  , styleOpacity :: !Float
  -- ^ Multiplied down the tree, not composited as a group.
  , styleFillOpacity :: !Float
  , styleStrokeOpacity :: !Float
  }
  deriving stock (Eq, Show)

-- | Unset fill, no stroke, width 1, butt caps, miter joins, miter limit 4,
-- nonzero fill rule and full opacity.
defaultStyle :: Style
defaultStyle = Style Nothing (Just PaintNone) 1 CapButt JoinMiter 4 NonZero 1 1 1

data Shape = Shape
  { shapeSegments :: !(SmallArray Segment)
  , shapeTransform :: !Matrix
  -- ^ From the segments' coordinates into 'documentViewBox'.
  , shapeStyle :: !Style
  }
  deriving stock (Eq, Show)

-- | A parsed document. Equality compares only 'documentKey'.
data Document = Document
  { documentViewBox :: !Box
  -- ^ The root @viewBox@, or else a box of the root size, 24 per missing side.
  , documentSize :: !(Float, Float)
  -- ^ The root @width@ and @height@, each defaulting to the viewBox's.
  , documentShapes :: !(SmallArray Shape)
  -- ^ In paint order.
  , documentKey :: !Int
  -- ^ FNV-1a hash of the source, for caching.
  , documentMonochrome :: !Bool
  -- ^ No shape has a literal 'PaintColor', so the drawing can be tinted.
  }

instance Eq Document where
  a == b = documentKey a == documentKey b

instance Show Document where
  show doc = "<svg " <> show (documentSize doc) <> ">"