packages feed

cadence-0.1.0.0: src/Cadence/Draw.hs

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}


module Cadence.Draw (draw,
                drawTexture,
                drawText,
                drawLine,
                drawPoint,
                drawRect,
                drawFilledRect) where

import qualified SDL
import qualified SDL.Font as TTF
import Apecs
import Cadence.Types
import Cadence.Texture
import Cadence.Font (RenText(..), FontMap(..))
import Cadence.Systems (getMaybe)
import Control.Monad.IO.Class (MonadIO)
import qualified Data.Vector.Mutable as MV
import qualified Data.Text as T
import qualified Data.Map as Map
import Linear
import Data.Word (Word8)
import Control.Monad (join, when)
import Data.Foldable (forM_)
import Data.Maybe (isNothing, fromJust)

{-|
Draw all 'Renderable' entities onto their appropriate layer.
This function is the typical draw function to pass to 'run', however you are free to implement your own
Every 'Renderable' Entity must have a 'Position' component. If a 'Renderable' Entity is set to not be visible, it is ignored
It is important to note that elements are drawn with top-left origin, and the x and y axes increase towards the right and down respectively.
-}
draw :: forall w.
      (Has w IO TextureMap
      , Get w IO Renderable
      , Get w IO FontMap
      , Get w IO Camera
      , Get w IO Position
      , Get w IO Config
      , Get w IO Layer
      , Has w IO IsVisible
      , Get w IO Colour)
      => SDL.Renderer
      -> FPS
      -> System w ()
draw renderer fps = do
    c <- get global
    Camera cam <- get global
    let (vw, vh) = windowDimensions c
        isInView :: V2 Float -> (Float,Float) -> Bool
        isInView pos (w,h) =
            let
                (V2 x y) = cam pos
                leftInView = x <= fromIntegral vw
                rightInView = x + w >= 0
                topInView = y <= fromIntegral vh
                bottomInView = y + h >= 0
            in
                leftInView && rightInView && topInView && bottomInView
    maxLayer <- cfold (\acc (_ :: Renderable, Position _, Layer n) -> max acc n) 0
    layerBuffers <- liftIO $ MV.replicateM (maxLayer + 1) (MV.new 1024) -- Preallocate mutable vectors for each layer
    layerCounts <- liftIO $ MV.replicate (maxLayer + 1) 0 -- Track the number of entities in each layer
    TextureMap tm <- get global
    FontMap fm <- get global
    -- Take an action to draw an entity on a given layer, and store it in the appropriate layer buffer
    let updateLayer :: Int -> System w () -> System w ()
        updateLayer i command = do
            buf <- liftIO $ MV.read layerBuffers i
            i' <- liftIO $ MV.read layerCounts i
            buf' <- if i' == MV.length buf then do
                    newBuf <- liftIO $ MV.grow buf (MV.length buf) -- Double the buffer size
                    liftIO $ MV.write layerBuffers i newBuf
                    return newBuf
                else return buf
            liftIO $ MV.write buf' i' command
            liftIO $ MV.write layerCounts i (i' + 1)
    -- Iterate through all Renderable entities, generate their draw commands and store them in the appropriate layer buffer
    cmapM_ $ \(r, Position pos, Layer n, e) -> case r of
        -- For each Renderable type, we do the following:
        -- 1. Check if the entity is visible by using a Proxy on the IsVisible component, if not we skip it
        -- 2. For Textures and Text, we check if their referenced resource exists in the TextureMap/FontMap, if not we skip it
        -- 3. Check if the entity is in view of the camera, if not we skip it
        -- 4. If the entity has a Colour component, we use that colour to draw it, otherwise we default to black
        -- 5. Add the drawing action to the appropriate layer buffer
        Texture t -> getMaybe e (Proxy @IsVisible) >>= \res -> when (isNothing res || (let IsVisible visible = fromJust res in visible)) $ case Map.lookup (textureRef t) tm of
            Just td -> do
                info <- liftIO $ SDL.queryTexture (texture td)
                IsVisible b <- get e
                let w = fromIntegral $ SDL.textureWidth info
                    h = fromIntegral $ SDL.textureHeight info
                when (isInView pos (w,h) && b) $ updateLayer n (drawTexture renderer td (cam pos) (animationFrame t))
            Nothing -> return () -- Texture not found, skip drawing
        Text t -> getMaybe e (Proxy @IsVisible) >>= \res -> when (isNothing res || (let IsVisible visible = fromJust res in visible)) $ case Map.lookup (fontRef t) fm of
            Just font -> do
                IsVisible b <- get e
                (w,h) <- liftIO $ TTF.size font (T.pack $ displayText t)
                when (isInView pos (fromIntegral w, fromIntegral h) && b) $ exists e (Proxy @Colour) >>= \c' -> if c' then
                    get e >>= \(Colour col) -> updateLayer n (drawText renderer t font (cam pos) col)
                else
                    updateLayer n (drawText renderer t font (cam pos) (V4 0 0 0 255))
            Nothing -> return () -- Font not found, skip drawing
        Point p -> getMaybe e (Proxy @IsVisible) >>= \res -> when (isNothing res || (let IsVisible visible = fromJust res in visible)) $ do
            IsVisible b <- get e
            when (isInView pos (0,0) && b) $ exists e (Proxy @Colour) >>= \c' -> if c' then
                get e >>= \(Colour col) -> updateLayer n (drawPoint renderer (cam pos) col)
            else
                updateLayer n (drawPoint renderer (cam pos) (V4 0 0 0 255))
        Line l -> getMaybe e (Proxy @IsVisible) >>= \res -> when (isNothing res || (let IsVisible visible = fromJust res in visible)) $ do
            IsVisible b <- get e
            when (isInView pos (lineX l, lineY l) && b) $ exists e (Proxy @Colour) >>= \c' -> if c' then
                get e >>= \(Colour col) -> updateLayer n (drawLine renderer l (cam pos) col)
            else
                updateLayer n (drawLine renderer l (cam pos) (V4 0 0 0 255))
        Rectangle r' -> getMaybe e (Proxy @IsVisible) >>= \res -> when (isNothing res || (let IsVisible visible = fromJust res in visible)) $ do
            IsVisible b <- get e
            let (V2 w h) = rectSize r'
            when (isInView pos (w,h) && b) $ exists e (Proxy @Colour) >>= \c' -> if c' then
                get e >>= \(Colour col) -> updateLayer n (drawRect renderer r' (cam pos) col)
            else
                updateLayer n (drawRect renderer r' (cam pos) (V4 0 0 0 255))
        FilledRectangle r' -> getMaybe e (Proxy @IsVisible) >>= \res -> when (isNothing res || (let IsVisible visible = fromJust res in visible)) $ do
            IsVisible b <- get e
            let (V2 w h) = rectSize r'
            when (isInView pos (w,h) && b) $ exists e (Proxy @Colour) >>= \c' -> if c' then
                get e >>= \(Colour col) -> updateLayer n (drawFilledRect renderer r' (cam pos) col)
            else
                updateLayer n (drawFilledRect renderer r' (cam pos) (V4 0 0 0 255))
    case showFPS c of
        Just ref -> case Map.lookup ref fm of
            Just font -> updateLayer maxLayer (drawText renderer (RenText ref (show fps ++ " FPS")) font (V2 10 10) (V4 0 255 0 255))
            Nothing -> return () -- Font not found, skip drawing FPS
        Nothing -> return () -- Not showing FPS, skip drawing it
    -- Iterate through each layer buffer and execute the drawing commands in order
    forM_ [0..maxLayer] $ \i -> do
        buf <- liftIO $ MV.read layerBuffers i
        count <- liftIO $ MV.read layerCounts i
        forM_ [0..(count - 1)] $ \j -> do
            join $ liftIO $ MV.read buf j -- Execute the drawing command

-- | Draw a line given its 'RenLine', 'Position' and colour
drawLine :: SDL.Renderer -> RenLine -> V2 Float -> V4 Word8 -> System w ()
drawLine r l pos col = do
    SDL.rendererDrawColor r SDL.$= col
    SDL.drawLine r (SDL.P $ floor <$> pos) (SDL.P $ floor <$> (pos + V2 (lineX l) (lineY l)))

-- | Draw a point given its 'Position' and colour
drawPoint :: SDL.Renderer -> V2 Float -> V4 Word8 -> System w ()
drawPoint r pos col = do
    SDL.rendererDrawColor r SDL.$= col
    SDL.drawPoint r (SDL.P $ floor <$> pos)

-- | Draw a rectangle outline given its 'RenRectangle', 'Position' and colour
drawRect :: SDL.Renderer -> RenRectangle -> V2 Float -> V4 Word8 -> System w ()
drawRect r rect pos col = do
    SDL.rendererDrawColor r SDL.$= col
    SDL.drawRect r $ Just (SDL.Rectangle (SDL.P (floor <$> pos)) (floor <$> rectSize rect))

-- | Draw a filled rectangle given its 'RenRectangle', 'Position' and colour
drawFilledRect :: SDL.Renderer -> RenRectangle -> V2 Float -> V4 Word8 -> System w ()
drawFilledRect r rect pos col = do
    SDL.rendererDrawColor r SDL.$= col
    SDL.fillRect r $ Just (SDL.Rectangle (SDL.P (floor <$> pos)) (floor <$> rectSize rect))

{-|
Draw a 'Texture' given its 'TextureData' and 'Position'
If either the 'TextureData' does not have an 'Animation' or no frame index is provided, the texture will be drawn as if it was static.
-}
drawTexture :: SDL.Renderer -> TextureData -> V2 Float -> Maybe Int -> System w ()
drawTexture r (TextureData t (Just a)) pos (Just n) = do
    info <- liftIO $ SDL.queryTexture t
    let w = SDL.textureWidth info
        h = SDL.textureHeight info
        fw = w `div` fromIntegral (frameCount a)
        srcRect = SDL.Rectangle (SDL.P (V2 (fromIntegral n * fw) 0)) (V2 fw h)
        dstRect = SDL.Rectangle (SDL.P (floor <$> pos)) (V2 fw h)
    liftIO $ SDL.copy r t (Just srcRect) (Just dstRect)
drawTexture r (TextureData t _) pos _ = do
    info <- liftIO $ SDL.queryTexture t
    let w = SDL.textureWidth info
        h = SDL.textureHeight info
        pos' = SDL.Rectangle (SDL.P (floor <$> pos)) (V2 w h)
    liftIO $ SDL.copy r t Nothing (Just pos')

-- | Draw text given its 'RenText', 'Font' and 'Position'
drawText :: SDL.Renderer -> RenText -> TTF.Font -> V2 Float -> V4 Word8 -> System w ()
drawText r t font pos col = do
    (tex, size) <- generateSolidText r font col (displayText t)
    SDL.copy r tex Nothing (Just $ SDL.Rectangle (SDL.P (floor <$> pos)) (fromIntegral <$> size))
    SDL.destroyTexture tex

generateSolidText :: MonadIO m => SDL.Renderer -> TTF.Font -> TTF.Color -> String -> m (SDL.Texture, SDL.V2 Int)
generateSolidText r font = generateText r font (TTF.solid font)

generateText :: MonadIO m => SDL.Renderer -> TTF.Font -> (TTF.Color -> T.Text -> m SDL.Surface) -> TTF.Color -> String -> m (SDL.Texture, SDL.V2 Int)
generateText r font f col str = do
    let t = T.pack str
    surface <- f col t
    tex <- liftIO $ SDL.createTextureFromSurface r surface
    SDL.freeSurface surface
    (w,h) <- liftIO $ TTF.size font t
    return (tex, V2 w h)