packages feed

aztecs-gl-0.2.1: src/Aztecs/GL/Sprite.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

module Aztecs.GL.Sprite
  ( -- * Sprites
    Sprite (..),
    sprite,
    spriteMaterial,

    -- * Sprite Sheets
    SpriteSheet (..),
  )
where

import Aztecs
import Aztecs.GL.Image.Internal
import Aztecs.GL.Internal
import Aztecs.GL.Material
import Aztecs.GL.Shape
import Aztecs.Transform
import Control.Monad
import Control.Monad.IO.Class
import Graphics.Rendering.OpenGL (($=))
import qualified Graphics.Rendering.OpenGL as GL
import Prelude hiding (lookup)

-- | Sprite component
data Sprite = Sprite
  { -- | Image component entity ID
    spriteImage :: !EntityID,
    -- | Clip rectangle of the sprite in the texture (top, left, width, height).
    -- If 'Nothing', uses full image.
    spriteClip :: Maybe (V4 Float),
    -- | Scale to apply to UV coordinates
    spriteScale :: !(V2 Float)
  }
  deriving (Eq)

instance (MonadIO m) => Component m Sprite where
  componentOnInsert e s = do
    mImageState <- lookup $ spriteImage s
    case mImageState of
      Just imgState@(ImageState _ imgSize@(V2 w h)) ->
        let (rectW, rectH) = case spriteClip s of
              Just (V4 _ _ clipW clipH) -> (clipW, clipH)
              Nothing -> (w, h)
         in insert e $ bundle (spriteMaterial s imgState imgSize) <> bundle (Rectangle rectW rectH)
      Nothing -> return ()
  componentOnChange e old new = when (old /= new) $ do
    mImageState <- lookup $ spriteImage new
    case mImageState of
      Just imgState@(ImageState _ imgSize@(V2 w h)) ->
        let (rectW, rectH) = case spriteClip new of
              Just (V4 _ _ clipW clipH) -> (clipW, clipH)
              Nothing -> (w, h)
         in insert e $ bundle (spriteMaterial new imgState imgSize) <> bundle (Rectangle rectW rectH)
      Nothing -> return ()

sprite :: EntityID -> Sprite
sprite imgE =
  Sprite
    { spriteImage = imgE,
      spriteClip = Nothing,
      spriteScale = V2 1 1
    }

-- | Create a sprite material with UV offset and scale based on sprite fields
spriteMaterial :: Sprite -> ImageState -> V2 Float -> Material
spriteMaterial s (ImageState tex _) (V2 imgW imgH) =
  let V2 scaleU scaleV = spriteScale s
      -- Calculate UV offset and scale based on clip rectangle
      (uvOffsetU, uvOffsetV, uvScaleU, uvScaleV) = case spriteClip s of
        Just (V4 top left clipW clipH) ->
          ( left / imgW,
            top / imgH,
            (clipW / imgW) * scaleU,
            (clipH / imgH) * scaleV
          )
        Nothing ->
          (0, 0, scaleU, scaleV)
   in Material $
        pure
          MaterialState
            { materialPush = do
                GL.texture GL.Texture2D $= GL.Enabled
                GL.textureBinding GL.Texture2D $= Just tex
                GL.textureFunction $= GL.Replace
                GL.color $ GL.Color4 1 1 1 (1 :: GL.GLfloat)

                -- Apply UV transform via texture matrix
                GL.matrixMode $= GL.Texture
                GL.loadIdentity
                GL.translate $ GL.Vector3 (realToFrac uvOffsetU) (realToFrac uvOffsetV) (0 :: GL.GLfloat)
                GL.scale (realToFrac uvScaleU) (realToFrac uvScaleV) (1 :: GL.GLfloat)
                GL.matrixMode $= GL.Modelview 0,
              materialPop = do
                -- Reset texture matrix
                GL.matrixMode $= GL.Texture
                GL.loadIdentity
                GL.matrixMode $= GL.Modelview 0

                GL.texture GL.Texture2D $= GL.Disabled
                GL.textureBinding GL.Texture2D $= Nothing
            }

-- | Sprite sheet component that controls a @Sprite@ on the same entity
data SpriteSheet = SpriteSheet
  { spriteSheetFrameIndex :: !(V2 Int),
    spriteSheetFrameSize :: !(V2 Int),
    spriteSheetFrameOffset :: !(V2 Int),
    spriteSheetFrameGap :: !(V2 Int)
  }

instance (MonadIO m) => Component m SpriteSheet where
  componentOnInsert = spriteSheetHook
  componentOnChange e _ = spriteSheetHook e

spriteSheetHook :: (MonadIO m) => EntityID -> SpriteSheet -> Access m ()
spriteSheetHook e sheet = do
  mSprite <- lookup e
  case mSprite of
    Just s ->
      let clip = spriteSheetClip sheet
       in insert e $ bundle s {spriteClip = Just clip}
    Nothing -> return ()

spriteSheetClip :: SpriteSheet -> V4 Float
spriteSheetClip sheet =
  let V2 frameX frameY = spriteSheetFrameIndex sheet
      V2 frameW frameH = spriteSheetFrameSize sheet
      V2 offsetX offsetY = spriteSheetFrameOffset sheet
      V2 gapX gapY = spriteSheetFrameGap sheet
      left = offsetX + frameX * (frameW + gapX)
      top = offsetY + frameY * (frameH + gapY)
   in V4 (fromIntegral top) (fromIntegral left) (fromIntegral frameW) (fromIntegral frameH)