{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DefaultSignatures #-} module Animate ( Color , FrameIndex , Frame(..) , Animations(..) , Loop(..) , Position(..) , FrameStep(..) , KeyName(..) , SpriteClip(..) , SpriteSheet(..) , SpriteSheetInfo(..) , animations , framesByAnimation , initPosition , initPositionLoops , initPositionWithLoop , stepFrame , stepPosition , isAnimationComplete , positionHasLooped , currentFrame , currentLocation , nextKey , prevKey , readSpriteSheetInfoJSON , readSpriteSheetInfoYAML , readSpriteSheetJSON , readSpriteSheetYAML ) where import qualified Data.Vector as V (Vector, (!), length, fromList) import qualified Data.Map as Map import qualified Data.ByteString.Lazy as BL import qualified Data.Yaml as Y import Control.Applicative ((<|>)) import Control.Monad (mzero) import Data.Aeson (FromJSON(..), ToJSON(..), (.:), eitherDecode, object, (.=), Value(..)) import Data.Map (Map) import Data.Word (Word8) import Data.Text (Text, pack) import GHC.Generics (Generic) -- | Alias for RGB (8bit, 8bit, 8bit) type Color = (Word8, Word8, Word8) type FrameIndex = Int data Frame loc delay = Frame { fLocation :: loc -- ^ User defined reference to the location of a sprite. For example, a sprite sheet clip. , fDelay :: delay -- ^ Minimium amount of units for the frame to last. } deriving (Show, Eq, Generic) -- | Type safe animation set. Use a sum type with an `Enum` and `Bounded` instance for the animation, @a@. newtype Animations key loc delay = Animations { unAnimations :: V.Vector (V.Vector (Frame loc delay)) } deriving (Show, Eq) -- class (Ord key, Bounded key, Enum key) => Key key -- | Animation Keyframe. `keyName` is used for JSON parsing. class KeyName key where keyName :: key -> Text default keyName :: Show key => key -> Text keyName = pack . dropTickPrefix . show where dropTickPrefix :: String -> String dropTickPrefix = drop 1 . dropWhile (/= '\'') -- | Describe the boxed area of the 2d sprite inside a sprite sheet data SpriteClip key = SpriteClip { scX :: Int , scY :: Int , scW :: Int , scH :: Int , scOffset :: Maybe (Int, Int) } deriving (Show, Eq, Generic) instance ToJSON (SpriteClip key) where toJSON SpriteClip{scX,scY,scW,scH,scOffset} = case scOffset of Nothing -> toJSON (scX, scY, scW, scH) Just (ofsX, ofsY) -> toJSON (scX, scY, scW, scH, ofsX, ofsY) instance FromJSON (SpriteClip key) where parseJSON v = (do (x,y,w,h) <- parseJSON v return SpriteClip { scX = x, scY = y, scW = w, scH = h, scOffset = Nothing }) <|> (do (x,y,w,h,ofsX,ofsY) <- parseJSON v return SpriteClip { scX = x, scY = y, scW = w, scH = h, scOffset = Just (ofsX, ofsY) }) -- | Generalized sprite sheet data structure data SpriteSheet key img delay = SpriteSheet { ssAnimations :: Animations key (SpriteClip key) delay , ssImage :: img } deriving (Generic) -- | One way to represent sprite sheet information. -- JSON loading is included. data SpriteSheetInfo key delay = SpriteSheetInfo { ssiImage :: FilePath , ssiAlpha :: Maybe Color , ssiClips :: [SpriteClip key] , ssiAnimations :: Map Text [(FrameIndex, delay)] } deriving (Show, Eq, Generic) instance ToJSON delay => ToJSON (SpriteSheetInfo key delay) where toJSON SpriteSheetInfo{ssiImage,ssiAlpha,ssiClips,ssiAnimations} = object [ "image" .= ssiImage , "alpha" .= ssiAlpha , "clips" .= ssiClips , "animations" .= ssiAnimations ] instance FromJSON delay => FromJSON (SpriteSheetInfo key delay) where parseJSON (Object o) = do image <- o .: "image" alpha <- o .: "alpha" clips <- o .: "clips" anis <- o .: "animations" return SpriteSheetInfo { ssiImage = image, ssiAlpha = alpha, ssiClips = clips, ssiAnimations = anis } parseJSON _ = mzero -- | Generate animations given each constructor animations :: (Enum key, Bounded key) => (key -> [Frame loc delay]) -> Animations key loc delay animations getFrames = Animations $ V.fromList $ map (V.fromList . getFrames) [minBound..maxBound] -- | Lookup the frames of an animation framesByAnimation :: Enum key => Animations key loc delay -> key -> V.Vector (Frame loc delay) framesByAnimation (Animations as) k = as V.! fromEnum k data Loop = Loop'Always -- ^ Never stop looping. Animation can never be completed. | Loop'Count Int -- ^ Count down loops to below zero. 0 = no loop. 1 = one loop. 2 = two loops. etc. deriving (Show, Eq, Generic) -- | State for progression through an animation -- -- > example = Position minBound 0 0 Loop'Always data Position key delay = Position { pKey :: key -- ^ Index for the animation. , pFrameIndex :: FrameIndex -- ^ Index wihin the animation. WARNING: Modifying to below zero or equal-to-or-greater-than-the-frame-count will throw out of bounds errors. , pCounter :: delay -- ^ Accumulated units to end of the frame. Will continue to compound if animation is completed. , pLoop :: Loop -- ^ How to loop through an animation. Loop'Count is a count down. } deriving (Show, Eq, Generic) -- | New `Position` with its animation key to loop forever initPosition :: (Num delay) => key -> Position key delay initPosition key = initPositionWithLoop key Loop'Always -- | New `Position` with its animation key with a limited loop initPositionLoops :: (Num delay) => key -> Int -> Position key delay initPositionLoops key count = initPositionWithLoop key (Loop'Count count) -- | New `Position` initPositionWithLoop :: (Num delay) => key -> Loop -> Position key delay initPositionWithLoop key loop = Position { pKey = key , pFrameIndex = 0 , pCounter = 0 , pLoop = loop } -- | You can ignore. An intermediate type for `stepPosition` to judge how to increment the current frame. data FrameStep delay = FrameStep'Counter delay -- ^ New counter to compare against the frame's delay. | FrameStep'Delta delay -- ^ How much delta to carry over into the next frame. deriving (Show, Eq, Generic) -- | Intermediate function for how a frame should be step through. stepFrame :: (Num delay, Ord delay) => Frame loc delay -> Position key delay -> delay -> FrameStep delay stepFrame Frame{fDelay} Position{pCounter} delta = if pCounter + delta >= fDelay then FrameStep'Delta $ pCounter + delta - fDelay else FrameStep'Counter $ pCounter + delta -- | Step through the animation resulting a new position. stepPosition :: (Enum key, Num delay, Ord delay) => Animations key loc delay -> Position key delay -> delay -> Position key delay stepPosition as p d = case frameStep of FrameStep'Counter counter -> p{pCounter = counter } FrameStep'Delta delta -> stepPosition as p' delta where frameStep = stepFrame f p d fs = unAnimations as V.! fromEnum (pKey p) f = fs V.! pFrameIndex p p'= case pLoop p of Loop'Always -> p{pFrameIndex = (pFrameIndex p + 1) `mod` V.length fs, pCounter = 0} Loop'Count n -> let index = (pFrameIndex p + 1) `mod` V.length fs n' = if index == 0 then n - 1 else n in p { pFrameIndex = if n' < 0 then pFrameIndex p else index , pCounter = 0 , pLoop = Loop'Count n' } -- | Use the position to find the current frame of the animation. currentFrame :: (Enum key, Num delay) => Animations key loc delay -> Position key delay -> Frame loc delay currentFrame anis Position{pKey,pFrameIndex} = (framesByAnimation anis pKey) V.! pFrameIndex -- | Use the position to find the current location, lik a sprite sheet clip, of the animation. currentLocation :: (Enum key, Num delay) => Animations key loc delay -> Position key delay -> loc currentLocation anis p = fLocation (currentFrame anis p) -- | The animation has finished all its frames. Useful for signalling into switching to another animation. -- With a Loop'Always, the animation will never be completed. isAnimationComplete :: (Enum key, Num delay, Ord delay) => Animations key loc delay -> Position key delay -> Bool isAnimationComplete as p = case pLoop p of Loop'Always -> False Loop'Count n -> n < 0 && pFrameIndex p == lastIndex && pCounter p >= fDelay lastFrame where frames = framesByAnimation as (pKey p) lastIndex = V.length frames - 1 lastFrame = frames V.! lastIndex -- | Cycle through the next animation key. nextKey :: (Bounded key, Enum key, Eq key) => key -> key nextKey key = if key == maxBound then minBound else succ key -- | Cycle through the previous animation key. prevKey :: (Bounded key, Enum key, Eq key) => key -> key prevKey key = if key == minBound then maxBound else pred key -- | Simple function diff'ing the position for loop change. positionHasLooped :: Position key delay -- ^ Previous -> Position key delay -- ^ Next -> Bool positionHasLooped Position{ pLoop = Loop'Count c } Position{ pLoop = Loop'Count c' } = c > c' positionHasLooped Position{ pLoop = Loop'Always } _ = False positionHasLooped _ Position{ pLoop = Loop'Always } = False -- | Quick function for loading `SpriteSheetInfo`. -- Check the example. readSpriteSheetInfoJSON :: FromJSON delay => FilePath -- ^ Path of the sprite sheet info JSON file -> IO (SpriteSheetInfo key delay) readSpriteSheetInfoJSON = readSpriteSheetInfo eitherDecode readSpriteSheetInfoYAML :: FromJSON delay => FilePath -- ^ Path of the sprite sheet info JSON file -> IO (SpriteSheetInfo key delay) readSpriteSheetInfoYAML = readSpriteSheetInfo eitherDecodeYAML eitherDecodeYAML :: FromJSON a => BL.ByteString -> Either String a eitherDecodeYAML = Y.decodeEither . BL.toStrict readSpriteSheetInfo :: FromJSON delay => (BL.ByteString -> Either String (SpriteSheetInfo key delay)) -> FilePath -- ^ Path of the sprite sheet info JSON file -> IO (SpriteSheetInfo key delay) readSpriteSheetInfo decoder path = do metaBytes <- BL.readFile path case decoder metaBytes of Left _err -> error $ "Cannot parse Sprite Sheet Info \"" ++ path ++ "\"" Right ssi -> return ssi -- | Quick function for loading `SpriteSheetInfo`, then using it to load its image for a `SpriteSheet`. -- Check the example. readSpriteSheetJSON :: (KeyName key, Ord key, Bounded key, Enum key, FromJSON delay) => (FilePath -> Maybe Color -> IO img) -- ^ Inject an image loading function -> FilePath -- ^ Path of the sprite sheet info JSON file -> IO (SpriteSheet key img delay) readSpriteSheetJSON = readSpriteSheet eitherDecode readSpriteSheetYAML :: (KeyName key, Ord key, Bounded key, Enum key, FromJSON delay) => (FilePath -> Maybe Color -> IO img) -- ^ Inject an image loading function -> FilePath -- ^ Path of the sprite sheet info JSON file -> IO (SpriteSheet key img delay) readSpriteSheetYAML = readSpriteSheet eitherDecodeYAML readSpriteSheet :: (KeyName key, Ord key, Bounded key, Enum key, FromJSON delay) => (BL.ByteString -> Either String (SpriteSheetInfo key delay)) -> (FilePath -> Maybe Color -> IO img) -> FilePath -> IO (SpriteSheet key img delay) readSpriteSheet decoder loadImage infoPath = do SpriteSheetInfo{ssiImage, ssiClips, ssiAnimations, ssiAlpha} <- readSpriteSheetInfo decoder infoPath i <- loadImage ssiImage ssiAlpha let frame key = (key, map (\a -> Frame (ssiClips !! fst a) (snd a)) (ssiAnimations Map.! keyName key)) let animationMap = Map.fromList $ map frame [minBound..maxBound] return $ SpriteSheet (animations $ (Map.!) animationMap) i