{-# 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 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.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