{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} -- | Description: Rendering composable picture fragments -- The module was originally inspired by gloss Picture data type. -- -- It allows you to define simple pieces, combine and move\/rotate\/scale them -- to produce the final image. -- -- Example of composite picture (filled circle inside stroked one, both moved to the side by 50 pixels): -- -- @ -- translateP 50 0 $ mconcat -- [ fill (Color 1 1 1 1) $ circle (10, 10) 10 -- , stroke (Color 1 1 1 1) $ circle (10, 10) 15 -- ] -- @ module Graphics.NanoVG.Picture ( -- * Shapes Shape (..) -- ** Helper type aliases , Radius , Point , Center , Angle -- ** Predefined Constructors , circle , line , rectangle , path , closedPath , arc , translateS , rotateS , scaleS , scaleS' , scaleSx , scaleSy , hole -- * Pictures , Picture , mapShape -- * Contstructors -- | Pictures are generally constructed by filling or stroking shape -- or by transforming exising picture. , stroke , fill , translateP , rotateP , scaleP , scaleP' , scalePx , scalePy -- * Rendering , render -- * Rendering as 'Window' , asWindow -- * Re-export , NVG.Color (..) ) where import Control.Exception.Safe import Data.Foldable import Graphics.NanoVG.Window (Window (..)) import qualified NanoVG as NVG -- | Radius of a circle or an arc. type Radius = Float -- | Point on 2D plane. type Point = (Float, Float) -- | Point representing center of circle, arc, rotation or scale. type Center = Point -- | Angle of rotation, arc, etc. type Angle = Float -- | Shape of a future picture fragment, to be filled or stroked later. -- Action should define set of paths for the passed 'NVG.Context'. newtype Shape = Shape { unShape :: NVG.Context -> IO () } deriving (Semigroup, Monoid) -- | Saves NanoVG state, applies modifications (first argument), -- runs actions (second argument) and restores state. -- TODO expose? withState :: NVG.Context -> IO () -> IO () -> IO () withState ctx t = bracket_ (NVG.save ctx *> t) (NVG.restore ctx) -- | Make circular shape. circle :: Center -> Radius -> Shape circle (x, y) r = Shape $ \ctx -> NVG.circle ctx (realToFrac x) (realToFrac y) (realToFrac r) -- | Make line shape. line :: Point -> Point -> Shape line (ax, ay) (bx, by) = Shape $ \ctx -> do NVG.moveTo ctx (realToFrac ax) (realToFrac ay) NVG.lineTo ctx (realToFrac bx) (realToFrac by) -- | Make rectangular shape given two (opposite) corners positions. rectangle :: Point -> Point -> Shape rectangle (ax, ay) (bx, by) = Shape $ \ctx -> NVG.rect ctx (realToFrac $ min ax bx) (realToFrac $ min ay by) (realToFrac $ abs $ ax - bx) (realToFrac $ abs $ ay - by) -- | Continuous path following the points path :: [Point] -> Shape path ps = Shape $ \ctx -> do case (ps, head ps) of (_:_:_, (x, y)) -> -- Extract first element but only if list has at least two NVG.moveTo ctx (realToFrac x) (realToFrac y) _ -> pure () forM_ (tail ps) $ \(x, y) -> NVG.lineTo ctx (realToFrac x) (realToFrac y) -- | Continuous path following the points then returning to the first one closedPath :: [Point] -> Shape closedPath ps = path ps <> case ps of ((x, y):_:_:_) -> Shape $ \ctx -> NVG.lineTo ctx (realToFrac x) (realToFrac y) _ -> Shape $ const $ pure () -- | Make arc shape with given center and going counter-clockwise -- from first angle to the second. arc :: Center -> Radius -> Angle -> Angle -> Shape arc (x, y) r a0 a1 = Shape $ \ctx -> NVG.arc ctx (realToFrac x) (realToFrac y) (realToFrac r) (realToFrac a0) (realToFrac a1) NVG.CCW -- | Translate shape by given @x@ and @y@ offsets. translateS :: Float -> Float -> Shape -> Shape translateS x y s = Shape $ \ctx -> withState ctx (NVG.translate ctx (realToFrac x) (realToFrac y)) $ unShape s ctx -- | Rotate shape around given point by given angle. rotateS :: Center -> Angle -> Shape -> Shape rotateS (x, y) a s = Shape $ \ctx -> withState ctx (NVG.translate ctx fx fy *> NVG.rotate ctx fa *> NVG.translate ctx (-fx) (-fy)) (unShape s ctx) where (fx, fy, fa) = (realToFrac x, realToFrac y, realToFrac a) -- | Scale shape from given point in given direction. -- This is affine transformation scaleS :: Center -> Angle -> Float -> Shape -> Shape scaleS (x, y) a k s = Shape $ \ctx -> withState ctx (NVG.translate ctx fx fy *> NVG.rotate ctx fa *> NVG.scale ctx fk 1 *> NVG.rotate ctx (-fa) *> NVG.translate ctx (-(fx*fk)) (-fy)) (unShape s ctx) where (fx, fy, fa, fk) = (realToFrac x, realToFrac y, realToFrac a, realToFrac k) -- | Scale shape from given point in positive X direction -- by given factor. scaleSx :: Center -> Float -> Shape -> Shape scaleSx (x, y) = scaleS (x, y) 0 -- | Scale shape from given point in positive Y direction -- by given factor. scaleSy :: Center -> Float -> Shape -> Shape scaleSy (x, y) = scaleS (x, y) (pi/2) -- | Scale shape from given point by given factor in every direction. scaleS' :: Center -> Float -> Shape -> Shape scaleS' c k = scaleSx c k . scaleSy c k -- | Turns shape into a hole which can then be combined -- with other (solid) shape. E.g. -- -- > fill (Color 1 0 0 1) $ circle (0, 0) 40 <> hole (circle (0, 0) 30) -- -- can be used to create a ring of width 10. hole :: Shape -> Shape hole s = Shape $ \ctx -> do unShape s ctx NVG.pathWinding ctx $ fromIntegral $ fromEnum NVG.CW -- | Picture represent collection of filled/stroked shapes -- ready to be rendered data Picture = Stroke NVG.Color Shape | Fill NVG.Color Shape | Pictures [Picture] instance Semigroup Picture where -- Could have get away with only last but let's try to reduce nesting Pictures ps1 <> Pictures ps2 = Pictures $ ps1 <> ps2 Pictures ps <> pic = Pictures $ ps <> [pic] pic <> Pictures ps = Pictures $ pic : ps pic1 <> pic2 = Pictures [pic1, pic2] instance Monoid Picture where mempty = Pictures [] -- | Modify shape(s) the picture was based off. mapShape :: (Shape -> Shape) -> Picture -> Picture mapShape f = \case Stroke c s -> Stroke c $ f s Fill c s -> Fill c $ f s Pictures ss -> Pictures $ mapShape f <$> ss -- | Translate the picture by given @x@ and @y@ offsets. translateP :: Float -> Float -> Picture -> Picture translateP x y = mapShape $ translateS x y -- | Rotate the picture around given point for given angle. rotateP :: Center -> Angle -> Picture -> Picture rotateP c a = mapShape $ rotateS c a -- | Scale picture from given point in given direction. -- This is affine transformation scaleP :: Center -> Angle -> Float -> Picture -> Picture scaleP c a k = mapShape $ scaleS c a k -- | Scale picture from given point in positive X direction -- by given factor. scalePx :: Center -> Float -> Picture -> Picture scalePx c = scaleP c 0 -- | Scale picture from given point in positive Y direction -- by given factor. scalePy :: Center -> Float -> Picture -> Picture scalePy c = scaleP c (pi/2) -- | Scale picture from given point by given factor in every direction. scaleP' :: Center -> Float -> Picture -> Picture scaleP' c k = scalePx c k . scalePy c k -- | Stroke the shape to create a picture. stroke :: NVG.Color -> Shape -> Picture stroke = Stroke -- | Fill the shape to create a picture. fill :: NVG.Color -> Shape -> Picture fill = Fill -- | Render picture with given NanoVG context. render :: NVG.Context -> Picture -> IO () render ctx = \case Stroke col s -> do NVG.beginPath ctx withState ctx (NVG.strokeColor ctx col *> NVG.strokeWidth ctx 1) $ unShape s ctx *> NVG.stroke ctx Fill col s -> do NVG.beginPath ctx withState ctx (NVG.fillColor ctx col) $ unShape s ctx *> NVG.fill ctx Pictures ss -> traverse_ (render ctx) ss -- | Create 'Window' which constantly queries and renders received picture. asWindow :: IO Picture -> Window () asWindow g = Window { winInit = \_ -> pure () , winRender = \_ ctx -> g >>= render ctx , winAfterRender = \_ _ -> pure () }