packages feed

sylvia-0.2.2: Sylvia/Render/Backend/Cairo.hs

-- |
-- Module      : Sylvia.Render.Backend.Cairo
-- Copyright   : GPLv3
--
-- Maintainer  : chrisyco@gmail.com
-- Portability : non-portable (requires FFI)
--
-- Render using the Cairo graphics library.

module Sylvia.Render.Backend.Cairo
    (
    -- * Types
      Image
    , Context(..)

    -- * Drawing the image
    , runImage
    , runImageWithPadding
    , writePNG

    -- * Testing
    , testRender

    -- * Re-exports
    , module Sylvia.Render.Backend
    ) where

import Control.Applicative
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import Data.Default
import Data.Lens.Common (getL)
import Data.Monoid (Monoid(..))
import Graphics.Rendering.Cairo

import Sylvia.Render.Backend
import Sylvia.Render.Core (render)
import Sylvia.Render.Pair
import Sylvia.Text.Parser

newtype Image = I { unI :: ImageM () }

type ImageM = ReaderT Context Render

-- | Render an image.
--
-- The resulting image's throat will be at (0, 0); in practice, this will
-- mean that if the result is run directly, most of the image would be
-- off the page. To fix this, use the 'translate' function or call
-- 'runImageWithPadding' instead.
runImage :: Context -> Image -> Render ()
runImage ctx = flip runReaderT ctx . unI

-- | Render an image, translating it so its top-left corner is at (0, 0).
--
-- If you only want to render the thing, this is the function to use.
runImageWithPadding
    :: Context
    -> (Image, PInt)     -- The image, along with its size in grid units
    -> (Render (), PInt) -- Rendering action, with its size in pixels
runImageWithPadding ctx (image, innerSize) = (action, realSize)
  where
    action = runImage ctx $ relativeTo (innerSize |+| (1 :| 1)) image
    realSize = ctxGridSize ctx |*| (innerSize |+| (2 :| 2))

-- | Lift a rendering action into the 'ImageM' monad, wrapping it in
-- calls to 'save' and 'restore' to stop its internal state from leaking
-- out.
cairo :: Render a -> ImageM a
cairo action = lift $ do
    save
    result <- action
    restore
    return result

-- | A 'Context' contains the environment required for rendering an
-- expression.
data Context = C
    { ctxGridSize :: PInt
      -- ^ The size of one grid unit. Whenever the rendering code
      -- specifies a coordinate, it is multiplied by this factor before
      -- it is drawn on the screen.
    , ctxOffset   :: PInt
      -- ^ The origin of the image. You shouldn't need to fiddle with
      -- this directly – try 'relativeTo' instead.
    }

instance Default Context where
    def = C { ctxGridSize = (20 :| 10), ctxOffset = (0 :| 0) }

-- | Map a relative coordinate to an absolute one, scaling and shifting
-- it in the process.
getAbsolute :: PInt -> ImageM PDouble
getAbsolute pair = getRelative =<< (pair |+|) <$> asks ctxOffset

-- | Scale a relative coordinate.
getRelative :: PInt -> ImageM PDouble
getRelative pair = fromIntegralP . (pair |*|) <$> asks ctxGridSize

-- | Add a half-pixel offset. This can make lines noticeably sharper, by
-- aligning points to the pixel grid.
addHalf :: PDouble -> PDouble
addHalf = (|+| (0.5 :| 0.5))

instance Monoid Image where
    mempty = I $ return ()
    I a `mappend` I b = I $ a >> b

instance Backend Image where
    drawDottedRectangle corner size = I $ do
        x  :| y  <- addHalf <$> getAbsolute corner
        dx :| dy <- getRelative size
        cairo $ do
            newPath
            rectangle x y dx dy
            setDash [1, 1] 0.5
            setLineWidth 1
            stroke

    drawLine src dest = I $ do
        x1 :| y1 <- addHalf <$> getAbsolute src
        x2 :| y2 <- addHalf <$> getAbsolute dest
        cairo $ do
            newPath
            moveTo x1 y1
            lineTo x2 y2
            setLineWidth 1
            stroke

    drawZigzag src dest = I $ do
        x1 :| y1 <- addHalf <$> getAbsolute src
        x2 :| y2 <- addHalf <$> getAbsolute dest
        let xmid = (x1 + x2) / 2
        cairo $ do
            newPath
            moveTo x1 y1
            lineTo xmid y1
            lineTo xmid y2
            lineTo x2 y2
            setLineWidth 1
            stroke

    drawCircleSegment center start end = I $ do
        cx :| cy <- getAbsolute center
        -- A dot's diameter is approximately equal to one vertical grid unit
        radius <- asks (fromIntegral . (`div` 2) . getL sndP . ctxGridSize)
        cairo $ do
            newPath
            arc cx cy radius start end
            setSourceRGB 0 0 0
            fill

    relativeTo delta = I . local addOffset . unI
      where
        addOffset ctx@C{ ctxOffset = offset }
          = ctx{ ctxOffset = offset |+| delta }

writePNG :: FilePath -> (Image, PInt) -> IO ()
writePNG filename imagePack = withImageSurface FormatRGB24 w h $ \surface -> do
    -- Fill the background with white
    renderWith surface $ setSourceRGB 1 1 1 >> paint
    -- Render ALL the things
    renderWith surface $ action
    -- Save the image
    surfaceWriteToPNG surface filename
  where
    (action, (w :| h)) = runImageWithPadding def imagePack

testRender :: IO ()
testRender = writePNG "result.png" . stackHorizontally $ map render es
  where
    es = map (fromRight . parseExp) $
        [ "L 0"
        , "LL 1"
        , "LL 0"
        , "LLL 2 0 (1 0)"
        , "(L 0 0) (L 0 0)"
        , "L (L 1 (0 0)) (L 1 (0 0))"
        ]

fromRight :: Show e => Either e a -> a
fromRight e = case e of
    Left sinister -> error $ "Unexpected Left: " ++ show sinister
    Right dextrous -> dextrous