comic-0.0.1: src/Data/Comic/Flip.hs
{-# LANGUAGE OverloadedStrings, DeriveGeneric, DeriveAnyClass, TemplateHaskell #-}
module Data.Comic.Flip (
Segment(..), ScheduledSegment(..)
, FrameSplice(..), TextType(..)
, OnScreen(..), Box(..)
, RelPosition(..), Font(..), blackRGBA, whiteRGBA
, TextLayout(..), HAlignment(..), VAlignment(..)
, osStart, osEnd
, fsWhen, fsDepth, fsBackground, fsFont, fsTxt, fsTxtType, fsTextLayout, fsWhere
, tlHorizontal, tlVertical, tlShrink
, sDuration, sSplice
) where
import Control.Applicative
import Control.DeepSeq
import Control.Lens
import Control.Monad
import qualified Data.Aeson as JS
import Data.Aeson (ToJSON(..), FromJSON(..), (.:))
import Data.Comic.Types
import Data.Foldable
import Data.Hashable (Hashable(hashWithSalt))
import Data.Hashable.Orphans ()
import Data.SortedList hiding (map)
import qualified Data.SortedList as Sorted
import Data.Text (Text)
import Data.Time
import Data.UUID (UUID)
import GHC.Generics (Generic)
type StartTime = Float
type EndTime = Float
data OnScreen =
OnScreen
{ _osStart :: {-# UNPACK #-} !StartTime
, _osEnd :: {-# UNPACK #-} !EndTime
}
deriving (Read, Show, Eq, Ord, Generic, NFData)
makeLenses ''OnScreen
instance Hashable OnScreen where
hashWithSalt s (OnScreen strt end) = hashWithSalt s (strt, end)
data Box =
Box
{ _bTl :: {-# UNPACK #-} !RelPosition
, _bBr :: {-# UNPACK #-} !RelPosition
}
deriving (Read, Show, Eq, Ord, Generic, NFData)
instance Hashable Box where
hashWithSalt s (Box tl br) = hashWithSalt s (tl, br)
data HAlignment =
HALeft
| HACenter
| HARight
deriving (Read, Show, Eq, Ord, Generic, NFData, Enum, Hashable)
instance ToJSON HAlignment where
toJSON HALeft = JS.String "left"
toJSON HACenter = JS.String "center"
toJSON HARight = JS.String "right"
instance FromJSON HAlignment where
parseJSON (JS.String "left") = pure HALeft
parseJSON (JS.String "center") = pure HACenter
parseJSON (JS.String "right") = pure HARight
parseJSON _ = mzero
data VAlignment =
VATop
| VACenter
| VABottom
deriving (Read, Show, Eq, Ord, Generic, NFData, Enum, Hashable)
instance ToJSON VAlignment where
toJSON VATop = JS.String "top"
toJSON VACenter = JS.String "center"
toJSON VABottom = JS.String "bottom"
instance FromJSON VAlignment where
parseJSON (JS.String "top") = pure VATop
parseJSON (JS.String "center") = pure VACenter
parseJSON (JS.String "bottom") = pure VABottom
parseJSON _ = mzero
data TextLayout =
TextLayout
{ _tlHorizontal :: !HAlignment
, _tlVertical :: !VAlignment
, _tlShrink :: !Bool
}
deriving (Read, Show, Eq, Ord, Generic, NFData, Hashable)
makeLenses ''TextLayout
data TextType =
TxtStatic
| TxtScroll
| TxtSpeech
| TxtBreaking
deriving (Read, Show, Eq, Ord, Generic, NFData, Enum, Hashable)
instance ToJSON TextType where
toJSON TxtStatic = JS.String "static"
toJSON TxtScroll = JS.String "scroll"
toJSON TxtSpeech = JS.String "speech"
toJSON TxtBreaking = JS.String "breaking"
instance FromJSON TextType where
parseJSON (JS.String "static") = return TxtStatic
parseJSON (JS.String "scroll") = return TxtScroll
parseJSON (JS.String "speech") = return TxtSpeech
parseJSON (JS.String "breaking") = return TxtBreaking
parseJSON _ = mzero
data FrameSplice =
FSBackground
{ _fsWhen :: {-# UNPACK #-} !OnScreen
, _fsDepth :: {-# UNPACK #-} !Int
, _fsBackground :: {-# UNPACK #-} !ImageName
}
| FSText
{ _fsWhen :: {-# UNPACK #-} !OnScreen
, _fsWhere :: {-# UNPACK #-} !Box
, _fsTxt :: {-# UNPACK #-} !Text
, _fsFont :: {-# UNPACK #-} !Font
, _fsTxtType :: !TextType
, _fsTextLayout :: !TextLayout
}
deriving (Read, Show, Eq, Ord, Generic, NFData)
makeLenses ''FrameSplice
instance Hashable FrameSplice where
hashWithSalt s (FSBackground o d i) = hashWithSalt s (o, d, i)
hashWithSalt s (FSText o w t f tt a) =
hashWithSalt s (o, w, t, f, tt, a)
instance ToJSON FrameSplice where
toJSON (FSBackground (OnScreen strt end) d img) =
JS.object $
[ ("start", toJSON strt), ("end", toJSON end)
, ("depth", toJSON d)
, ("panel", toJSON img)
]
toJSON (FSText (OnScreen strt end) (Box tl br) txt (Font fn fs c) tt (TextLayout h v s)) =
JS.object $
[ ("start", toJSON strt), ("end", toJSON end)
, ("tl", toJSON tl), ("br", toJSON br)
, ("font", toJSON fn), ("size", toJSON fs), ("color", toJSON c)
, ("txt", toJSON txt)
, ("txt_type", toJSON tt)
, ("align", toJSON h), ("valign", toJSON v), ("shrink_fit", toJSON s)
]
instance FromJSON FrameSplice where
parseJSON (JS.Object v) =
(FSBackground <$> (OnScreen <$> v .: "start" <*> v .: "end")
<*> v .: "depth"
<*> v .: "panel")
<|> (FSText <$> (OnScreen <$> v .: "start" <*> v .: "end")
<*> (Box <$> v .: "tl" <*> v .: "br")
<*> v .: "txt"
<*> (Font <$> v .: "font" <*> v .: "size" <*> v .: "color")
<*> v .: "txt_type"
<*> (TextLayout <$> v .: "align" <*> v .: "valign" <*> v .: "shrink_fit"))
parseJSON _ = mzero
data Segment =
Segment
{ _sDuration :: !DiffTime
-- ^ This is how long, ideally, the segment would air for.
, _sSplice :: SortedList FrameSplice
-- ^ The actual stage directions for playing the content.
-- Times are [0, 1] of the Duration.
-- We keep sorting for a stable Ord instance.
}
deriving (Show, Eq, Ord, Generic, NFData)
makeLenses ''Segment
instance Semigroup Segment where
(<>) a b = mconcat [a, b]
instance Monoid Segment where
mempty = Segment 0 mempty
mappend a b = mconcat [a, b]
mconcat os =
let totTime = fromRational . toRational . foldl (\a s -> a + _sDuration s) 0 $ os
in Segment (fromRational . toRational $ totTime)
(snd . foldl (\(c, fs) s ->
let sdur = fromRational . toRational $ s ^. sDuration
timePrev = c+sdur
segPercent = sdur / totTime
in (timePrev
,fs `mappend` (Sorted.map (\f ->
-- We rescale the [0-1] time range into the percent that this
-- Segment forms of the overal, and shift it into position.
((f & fsWhen.osStart %~ (\st -> (st*sdur+c)/totTime))
& fsWhen.osEnd %~ (\se -> (se*sdur+c)/totTime))) (_sSplice $ s))
)) (0, mempty) $ os)
instance Hashable Segment where
hashWithSalt s (Segment dur spl) =
hashWithSalt s (dur, spl)
instance ToJSON Segment where
toJSON (Segment dur splcs) =
JS.object $
[ ("duration", toJSON (realToFrac dur::Float))
, ("splices", toJSON . toList $ splcs)
]
instance FromJSON Segment where
parseJSON (JS.Object v) =
Segment <$> ((realToFrac::Float -> DiffTime) <$> v .: "duration")
<*> (toSortedList <$> v .: "splices")
parseJSON _ = mzero
data ScheduledSegment =
ScheduledSegment
{ _ssSegment :: {-# UNPACK #-} !Segment
, _ssSegID :: {-# UNPACK #-} !UUID
, _ssPlayTill :: !DiffTime
-- ^ This is how long in the future this segment should end,
-- from the time it is transmited to the client.
}
deriving (Show, Eq, Ord, Generic, NFData)
instance ToJSON ScheduledSegment where
toJSON (ScheduledSegment seg segid till) =
JS.object
[ ("till", toJSON (realToFrac till::Float))
, ("id", toJSON segid)
, ("segment", toJSON seg)
]
instance FromJSON ScheduledSegment where
parseJSON (JS.Object v) =
ScheduledSegment <$> v .: "segment"
<*> v .: "id"
<*> ((realToFrac::Float -> DiffTime) <$> v .: "till")
parseJSON _ = mzero