cairo-canvas-0.1.0.1: src/Graphics/Rendering/Cairo/Canvas.hs
{-# LANGUAGE CPP,GeneralizedNewtypeDeriving,RankNTypes #-}
{-|
Module : Graphics.Rendering.Cairo.Canvas
Copyright : Copyright (c) 2015 Anton Pirogov
License : MIT
Maintainer : anton.pirogov@gmail.com
This module defines the 'Canvas' monad, which is a convenience wrapper around
the underlying Cairo rendering and can be used with the same textures.
You can also mix both APIs, if the need arises.
The Canvas API imitates most of the drawing functions
of the Processing language. See <http://processing.org/reference> for comparison.
While having the Processing spirit, this module does not aim for a perfect
mapping and deviates where necessary or appropriate. Nevertheless most
Processing examples should be trivial to port to the Canvas API. Example:
@
\{\-\# LANGUAGE OverloadedStrings \#\-\}
import SDL
import SDL.Cairo
import Linear.V2 (V2(..))
import Graphics.Rendering.Cairo.Canvas
main :: IO ()
main = do
initializeAll
window <- createWindow "cairo-canvas using SDL2" defaultWindow
renderer <- createRenderer window (-1) defaultRenderer
texture <- createCairoTexture' renderer window
withCairoTexture' texture $ runCanvas $ do
background $ gray 102
fill $ red 255 !\@ 128
noStroke
rect $ D 200 200 100 100
stroke $ green 255 !\@ 128
fill $ blue 255 !\@ 128
rect $ D 250 250 100 100
triangle (V2 400 300) (V2 350 400) (V2 400 400)
copy renderer texture Nothing Nothing
present renderer
delay 5000
@
-}
module Graphics.Rendering.Cairo.Canvas (
-- * Entry point
Canvas, runCanvas, withRenderer, getCanvasSize,
-- * Color and Style
Color, Byte, gray, red, green, blue, rgb, (!@),
stroke, fill, noStroke, noFill, strokeWeight, strokeJoin, strokeCap,
-- * Coordinates
Dim(..), toD, dimPos, dimSize, Anchor(..), aligned, centered, corners,
-- * Primitives
background, point, line, triangle, rect, polygon, shape, ShapeMode(..),
-- * Arcs and Curves
circle, circle', arc, ellipse, bezier, bezierQ,
-- * Transformations
resetMatrix, pushMatrix, popMatrix, translate, rotate, scale,
-- * Images
Image(imageSize), createImage, loadImagePNG, saveImagePNG, image, image', blend, grab,
-- * Text
Font(..), textFont, textSize, textExtents, text, text',
-- * Math
mapRange, radians, degrees,
-- * Misc
randomSeed, random, getTime, Time(..),
LineCap(..), LineJoin(..)
) where
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Monad (when, forM_, (<=<))
import Control.Monad.State
import Data.Word (Word8)
import Data.Time.Clock (UTCTime(..),getCurrentTime)
import Data.Time.LocalTime (timeToTimeOfDay,TimeOfDay(..))
import Data.Time.Calendar (toGregorian)
import System.Random (mkStdGen,setStdGen,randomRIO,Random)
import Linear.V2 (V2(..))
import Linear.V4 (V4(..))
import qualified Graphics.Rendering.Cairo as C
import Graphics.Rendering.Cairo (Render,LineJoin(..),LineCap(..),Format(..),Operator(..))
-- | For values from 0 to 255
type Byte = Word8
-- | RGBA Color is just a byte vector. Colors can be added, subtracted, etc.
type Color = V4 Byte
data CanvasState = CanvasState{ csSize :: V2 Double, -- ^ reported size
csFG :: Maybe Color, -- ^ stroke color
csBG :: Maybe Color, -- ^ fill color
csImages :: [Image] -- ^ keep track of images to free later
}
-- | get size of the canvas (Processing: @width(), height()@)
getCanvasSize :: Canvas (V2 Double)
getCanvasSize = gets csSize
newtype RenderWrapper m a = Canvas { unCanvas :: StateT CanvasState m a }
deriving (Functor, Applicative, Monad, MonadTrans, MonadIO, MonadState CanvasState)
-- | wrapper around the Cairo 'Render' monad, providing a Processing-style API
type Canvas = RenderWrapper Render
-- | draw on a Cairo surface using the 'Canvas' monad
runCanvas = flip withCairoSurface
withCairoSurface :: C.Surface -> Canvas a -> IO a
withCairoSurface s m = do
w <- fromIntegral <$> C.imageSurfaceGetWidth s
h <- fromIntegral <$> C.imageSurfaceGetHeight s
withRenderer (C.renderWith s) (V2 w h) m
-- | draw on a Cairo surface using the 'Canvas' monad
withRenderer :: (forall a. Render a -> IO a) -- ^ the renderer to use (e.g. 'Graphics.Rendering.Cairo.renderWith' surface)
-> V2 Double -- ^ reported canvas size
-> Canvas a -> IO a
withRenderer renderer size c = do
let defaults = strokeWeight 1 >> strokeCap C.LineCapRound
initstate = CanvasState{ csSize = size
, csFG = Just $ gray 0
, csBG = Just $ gray 255
, csImages = []
}
(ret, result) <- renderer $ runStateT (unCanvas $ defaults >> c) initstate
forM_ (csImages result) $ \(Image s' _ _) -> C.surfaceFinish s'
return ret
----
-- | set current stroke color
stroke :: Color -> Canvas ()
stroke clr = modify $ \cs -> cs{csFG=Just clr}
-- | set current fill color
fill :: Color -> Canvas ()
fill clr = modify $ \cs -> cs{csBG=Just clr}
-- | disable stroke (-> shapes without borders!), reenabled by using 'stroke'
noStroke :: Canvas ()
noStroke = modify $ \cs -> cs{csFG=Nothing}
-- | disable fill (-> shapes are not filled!), reenabled by using 'fill'
noFill :: Canvas ()
noFill = modify $ \cs -> cs{csBG=Nothing}
-- | create opaque gray color
gray :: Byte -> Color
gray c = V4 c c c 255
-- | create opaque red color
red :: Byte -> Color
red c = V4 c 0 0 255
-- | create opaque green color
green :: Byte -> Color
green c = V4 0 c 0 255
-- | create opaque blue color
blue :: Byte -> Color
blue c = V4 0 0 c 255
-- | create opaque mixed color
rgb :: Byte -> Byte -> Byte -> Color
rgb r g b = V4 r g b 255
-- | set transparency of color (half red would be: @red 255 !\@ 128@)
(!@) :: Color -> Byte -> Color
(V4 r g b _) !@ a = V4 r g b a
-- | set line width for shape borders etc.
strokeWeight :: Double -> Canvas ()
strokeWeight d = lift $ C.setLineWidth d
-- | set the style of connections between lines of shapes
strokeJoin :: C.LineJoin -> Canvas ()
strokeJoin l = lift $ C.setLineJoin l
-- | set the style of the line caps
strokeCap :: C.LineCap -> Canvas ()
strokeCap l = lift $ C.setLineCap l
----
-- | position (canonically, top-left corner) and size representation (X Y W H)
data Dim = D Double Double Double Double deriving (Show,Eq)
-- | indicates where a position coordinate is located in a rectangle
data Anchor = NW | N | NE | E | SE | S | SW | W | Center | Baseline deriving (Show,Eq)
-- | create dimensions from position and size vector
toD (V2 a b) (V2 c d) = D a b c d
-- | get position vector from dimensions
dimPos (D a b _ _) = V2 a b
-- | get size vector from dimensions
dimSize (D _ _ c d) = V2 c d
-- | takes dimensions with bottom-right corner instead of size, returns normalized (with size)
corners (D xl yl xh yh) = D xl yl (xh-xl) (yh-yl)
-- | takes dimensions with centered position, returns normalized (top-left corner)
centered = aligned Center
-- | takes dimensions with non-standard position coordinate,
-- returns dimensions normalized to top-left corner coordinate
aligned :: Anchor -> Dim -> Dim
aligned NW dim = dim
aligned NE (D x y w h) = D (x-w) y w h
aligned SW (D x y w h) = D x (y-h) w h
aligned SE (D x y w h) = D (x-w) (y-h) w h
aligned Baseline dim = aligned SW dim
aligned N (D x y w h) = D (x-w/2) y w h
aligned W (D x y w h) = D x (y-h/2) w h
aligned S (D x y w h) = D (x-w/2) (y-h) w h
aligned E (D x y w h) = D (x-w) (y-h/2) w h
aligned Center (D x y w h) = D (x-w/2) (y-h/2) w h
----
-- | replace current matrix with identity
resetMatrix :: Canvas ()
resetMatrix = lift C.identityMatrix
-- | push current matrix onto the stack
pushMatrix :: Canvas ()
pushMatrix = lift C.save
-- | pop a matrix
popMatrix :: Canvas ()
popMatrix = lift C.restore
-- | translate coordinate system
translate :: V2 Double -> Canvas ()
translate (V2 x y) = lift $ C.translate x y
-- | scale coordinate system
scale :: V2 Double -> Canvas ()
scale (V2 x y) = lift $ C.scale x y
-- | rotate coordinate system
rotate :: Double -> Canvas ()
rotate a = lift $ C.rotate a
----
-- | clear the canvas with given color
background :: Color -> Canvas ()
background c = do
(V2 w h) <- gets csSize
lift $ setColor c >> C.rectangle 0 0 w h >> C.fill
-- | draw a point with stroke color (cairo emulates this with 1x1 rects!)
point :: V2 Double -> Canvas ()
point (V2 x y) = ifColor csFG $ \c -> do
C.rectangle x y 1 1
setColor c
C.fill
-- | draw a line between two points with stroke color
line :: V2 Double -> V2 Double -> Canvas ()
line (V2 x1 y1) (V2 x2 y2) = ifColor csFG $ \c -> do
C.moveTo x1 y1
C.lineTo x2 y2
setColor c
C.stroke
-- | draw a triangle connecting three points
triangle :: V2 Double -> V2 Double -> V2 Double -> Canvas ()
triangle (V2 x1 y1) (V2 x2 y2) (V2 x3 y3) = drawShape $ do
C.moveTo x1 y1
C.lineTo x2 y2
C.lineTo x3 y3
C.lineTo x1 y1
-- | draw a rectangle
rect :: Dim -> Canvas ()
rect (D x y w h) = drawShape $ C.rectangle x y w h
-- | draw a polygon connecting given points (equivalent to @'shape' ('ShapeRegular' True)@)
polygon :: [V2 Double] -> Canvas ()
polygon = shape (ShapeRegular True)
-- | Shape mode to use
data ShapeMode = ShapeRegular Bool -- ^regular path. flag decides whether the first and last point are connected
| ShapePoints -- ^just draw the points, no lines
| ShapeLines -- ^interpret points as pairs, draw lines
| ShapeTriangles -- ^interpret points as triples, draw triangles
| ShapeTriangleStrip -- ^draw triangle for every neighborhood of 3 points
| ShapeTriangleFan -- ^fix first point, draw triangles with every neighboring pair and first point
deriving (Show,Eq)
-- | draw shape along a given path using given @'ShapeMode'@.
-- (Processing: @beginShape(),vertex(),endShape()@)
shape :: ShapeMode -> [V2 Double] -> Canvas ()
shape (ShapeRegular closed) ((V2 x y):ps) = drawShape $ do
C.moveTo x y
forM_ ps $ \(V2 x' y') -> C.lineTo x' y'
when closed $ C.closePath
shape (ShapeRegular _) _ = return ()
shape ShapePoints ps = forM_ ps point
shape ShapeLines (p1:p2:ps) = do
line p1 p2
shape ShapeLines ps
shape ShapeLines _ = return ()
shape ShapeTriangles (p1:p2:p3:ps) = do
triangle p1 p2 p3
shape ShapeTriangles ps
shape ShapeTriangles _ = return ()
shape ShapeTriangleStrip (p1:p2:p3:ps) = do
triangle p1 p2 p3
shape ShapeTriangleStrip (p2:p3:ps)
shape ShapeTriangleStrip _ = return ()
shape ShapeTriangleFan (p1:p2:p3:ps) = do
triangle p1 p2 p3
shape ShapeTriangleFan (p1:p3:ps)
shape ShapeTriangleFan _ = return ()
----
-- | draw arc: @arc dimensions startAngle endAngle@
arc :: Dim -> Double -> Double -> Canvas ()
arc (D x y w h) sa ea = drawShape $ do
C.save
C.translate (x+(w/2)) (y+(h/2))
C.scale (w/2) (h/2)
C.arc 0 0 1 sa ea
C.restore
-- | draw ellipse
ellipse :: Dim -> Canvas ()
ellipse dim = arc dim 0 (2*pi)
-- | draw circle: @circle leftCorner diameter@
circle :: V2 Double -> Double -> Canvas ()
circle (V2 x y) d = ellipse (D x y d d)
-- | draw circle: @circle centerPoint diameter@
circle' :: V2 Double -> Double -> Canvas ()
circle' (V2 x y) d = ellipse $ centered (D x y d d)
-- | draw cubic bezier spline: @bezier fstAnchor fstControl sndControl sndAnchor@
bezier :: V2 Double -> V2 Double -> V2 Double -> V2 Double -> Canvas ()
bezier (V2 x1 y1) (V2 x2 y2) (V2 x3 y3) (V2 x4 y4) = drawShape $ do
C.moveTo x1 y1
C.curveTo x2 y2 x3 y3 x4 y4
-- | draw quadratic bezier spline: @bezier fstAnchor control sndAnchor@
bezierQ :: V2 Double -> V2 Double -> V2 Double -> Canvas ()
bezierQ p0 p12 p3 = bezier p0 p1 p2 p3
where p1 = p0 + 2/3*(p12-p0)
p2 = p3 + 2/3*(p12-p3)
----
-- | map a value from one range onto another
mapRange :: Double -> (Double,Double) -> (Double,Double) -> Double
mapRange v (l1,r1) (l2,r2) = (v-l1)*fac + l2
where fac = (r2-l2)/(r1-l1)
-- | convert degrees to radians
radians :: Double -> Double
radians d = d*pi/180
-- | convert radians to degrees
degrees :: Double -> Double
degrees r = r/pi*180
-- | force value v into given range
constrain :: Double -> (Double,Double) -> Double
constrain v (l,h) = max l $ min h v
-- | set new random seed
randomSeed :: Int -> Canvas ()
randomSeed s = liftIO $ setStdGen $ mkStdGen s
-- | get new random number
random :: (Random a) => (a,a) -> Canvas a
random = liftIO . randomRIO
-- | date and time as returned by getTime
data Time = Time { year :: Int, month :: Int, day :: Int
, hour :: Int, minute :: Int, second :: Int } deriving (Show,Eq)
-- | get current system time. Use the 'Time' accessors for specific components.
-- (Processing: @year(),month(),day(),hour(),minute(),second()@)
getTime :: IO Time
getTime = do
(UTCTime day time) <- getCurrentTime
let (y,m,d) = toGregorian day
(TimeOfDay h mins s) = timeToTimeOfDay time
return $ Time (fromIntegral y::Int) m d h mins (round s :: Int)
----
-- | Stores an image surface with additional information
data Image = Image {imageSurface::C.Surface, imageSize::V2 Int, imageFormat::Format}
-- | create a new empty image of given size
createImage :: V2 Int -> Canvas Image
createImage (V2 w h) = do
s <- liftIO $ C.createImageSurface FormatARGB32 w h
let img = Image s (V2 w h) FormatARGB32
track img
return img
--TODO: add checks (file exists, correct format, etc.)
-- | load a PNG image from given path.
loadImagePNG :: FilePath -> Canvas Image
loadImagePNG path = do
s <- liftIO $ C.imageSurfaceCreateFromPNG path
w <- C.imageSurfaceGetWidth s
h <- C.imageSurfaceGetHeight s
f <- C.imageSurfaceGetFormat s
let img = Image s (V2 w h) f
track img
return img
-- | Save an image as PNG to given file path
saveImagePNG :: Image -> FilePath -> Canvas ()
saveImagePNG (Image s _ _) fp = liftIO (C.surfaceWriteToPNG s fp)
-- | Render complete image on given coordinates
image :: Image -> V2 Double -> Canvas ()
image img@(Image _ (V2 w h) _) (V2 x y) =
image' img (D x y (fromIntegral w) (fromIntegral h))
-- | Render complete image inside given dimensions
image' :: Image -> Dim -> Canvas ()
image' img@(Image _ (V2 ow oh) _) =
blend OperatorSource img (D 0 0 (fromIntegral ow) (fromIntegral oh))
-- | Copy given part of image to given part of screen, using given blending
-- operator and resizing when necessary. Use 'OperatorSource' to copy without
-- blending effects. (Processing: @copy(),blend()@)
blend :: Operator -> Image -> Dim -> Dim -> Canvas ()
blend op (Image s _ _) sdim ddim = lift $ C.withTargetSurface $ \surf ->
copyFromToSurface op s sdim surf ddim
-- | get a copy of the image from current window (Processing: @get()@)
grab :: Dim -> Canvas Image
grab dim@(D _ _ w h) = do
i@(Image s _ _) <- createImage (V2 (round w) (round h))
lift $ C.withTargetSurface $ \surf ->
copyFromToSurface OperatorSource surf dim s (D 0 0 w h)
return i
----
-- | Font definition
data Font = Font{fontFace::String
,fontSize::Double
,fontBold::Bool
,fontItalic::Bool} deriving (Show,Eq)
-- | set current font for text rendering
textFont :: Font -> Canvas ()
textFont f = lift $ setFont f
-- | get the size of the text when rendered in current font
textSize :: String -> Canvas (V2 Double)
textSize = return . dimSize . fst <=< textExtents
-- | get information about given text when rendered in current font.
-- returns tuple with location of top-left corner relative to
-- the origin and size of rendered text in the first component,
-- cursor advancement relative to origin in the second component
-- (also see 'Graphics.Rendering.Cairo.TextExtents').
textExtents :: String -> Canvas (Dim, V2 Double)
textExtents s = do
(C.TextExtents xb yb w h xa ya) <- lift $ C.textExtents s
return ((D xb yb w h),(V2 xa ya))
-- | render text. returns cursor advancement (@text = text' Baseline@)
text :: String -> V2 Double -> Canvas (V2 Double)
text = text' Baseline
-- | render text with specified alignment. returns cursor advancement
text' :: Anchor -> String -> V2 Double -> Canvas (V2 Double)
text' a str pos = do
(C.TextExtents xb yb w h xa ya) <- lift $ C.textExtents str
let (D xn yn _ _) = (if a==Baseline then id else aligned a) $ toD pos $ V2 w h
(V2 x' y') = (V2 xn yn) - if a/=Baseline then (V2 xb yb) else 0
ifColor csFG $ \c -> C.moveTo x' y' >> setColor c >> C.showText str
return $ V2 xa ya
-- helpers --
-- | draw a shape - first fill with bg color, then draw border with stroke color
drawShape :: Render a -> Canvas ()
drawShape m = do
ifColor csBG $ \c -> m >> setColor c >> C.fill
ifColor csFG $ \c -> m >> setColor c >> C.stroke
-- | if color (csFG/csBG) is set, perform given render block
ifColor :: (CanvasState -> Maybe Color) -> (Color -> Render a) -> Canvas ()
ifColor cf m = get >>= \cs -> case cf cs of
Just c -> lift (m c) >> return ()
Nothing -> return ()
-- | convert from 256-value RGBA to Double representation, set color
setColor :: Color -> Render ()
setColor (V4 r g b a) = C.setSourceRGBA (conv r) (conv g) (conv b) (conv a)
where conv = ((1.0/256)*).fromIntegral
-- | Add to garbage collection list
track :: Image -> Canvas ()
track img = modify $ \cs -> cs{csImages=img:csImages cs}
-- cairo helpers --
-- | helper: returns new surface with scaled content. does NOT cleanup!
createScaledSurface :: C.Surface -> (V2 Double) -> Render C.Surface
createScaledSurface s (V2 w h) = do
ow <- C.imageSurfaceGetWidth s
oh <- C.imageSurfaceGetHeight s
s' <- liftIO $ C.createSimilarSurface s C.ContentColorAlpha (round w) (round h)
C.renderWith s' $ do
C.scale (w/fromIntegral ow) (h/fromIntegral oh)
C.setSourceSurface s 0 0
pat <- C.getSource
C.patternSetExtend pat C.ExtendPad
C.setOperator C.OperatorSource
C.paint
return s'
-- | helper: returns new surface with only part of original content. does NOT cleanup!
createTrimmedSurface :: C.Surface -> Dim -> Render C.Surface
createTrimmedSurface s (D x y w h) = do
s' <- liftIO $ C.createSimilarSurface s C.ContentColorAlpha (round w) (round h)
C.renderWith s' $ do
C.setSourceSurface s (-x) (-y)
C.setOperator C.OperatorSource
C.rectangle 0 0 w h
C.fill
return s'
copyFromToSurface :: Operator -> C.Surface -> Dim -> C.Surface -> Dim -> Render ()
copyFromToSurface op src sdim@(D sx sy sw sh) dest (D x y w h) = do
ow <- C.imageSurfaceGetWidth src
oh <- C.imageSurfaceGetHeight src
let needsTrim = sx/=0 || sy/=0 || round sw/=ow || round sh/=oh
needsRescale = round sw/=round w || round sh/=round h
s' <- if needsTrim then createTrimmedSurface src sdim else return src
s'' <- if needsRescale then createScaledSurface s' (V2 w h) else return s'
C.renderWith dest $ do
C.save
C.setSourceSurface s'' x y
C.setOperator op
C.rectangle x y w h
C.fill
C.restore
when needsTrim $ C.surfaceFinish s'
when needsRescale $ C.surfaceFinish s''
-- | Set the current font
setFont :: Font -> Render ()
setFont (Font face sz bold italic) = do
C.selectFontFace face
(if italic then C.FontSlantItalic else C.FontSlantNormal )
(if bold then C.FontWeightBold else C.FontWeightNormal)
C.setFontSize sz