reanimate-0.3.2.0: examples/voice_transcript.hs
#!/usr/bin/env stack
-- stack --resolver lts-15.04 runghc --package reanimate
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
import Codec.Picture.Types
import Control.Monad
import Data.Hashable
import Data.Aeson
import Data.Char
import System.IO.Unsafe
import Data.Function
import Data.List
import Data.Maybe
import Data.Ratio
import qualified Data.Text as T
import Data.Tuple
import qualified Data.Vector as V
import Debug.Trace
import Reanimate
import Reanimate.Animation
import Reanimate.Interpolate
import Reanimate.Svg
import Graphics.SvgTree ( Texture(..)
, ElementRef(..)
)
data Transcript = Transcript
{ transcriptText :: T.Text
, transcriptWords :: [TWord]
} deriving (Show)
instance FromJSON Transcript where
parseJSON =
withObject "transcript" $ \o -> Transcript <$> o .: "transcript" <*> o .: "words"
data TWord = TWord
{ wordAligned :: T.Text
, wordCase :: T.Text
, wordStart :: Double
, wordStartOffset :: Int
, wordEnd :: Double
, wordEndOffset :: Int
, wordPhones :: [Phone]
, wordReference :: T.Text
} deriving (Show)
instance FromJSON TWord where
parseJSON = withObject "word" $ \o ->
TWord
<$> o
.:? "alignedWord"
.!= T.empty
<*> o
.: "case"
<*> o
.:? "start"
.!= 0
<*> o
.: "startOffset"
<*> o
.:? "end"
.!= 0
<*> o
.: "endOffset"
<*> o
.:? "phones"
.!= []
<*> o
.: "word"
data Phone = Phone
{ phoneDuration :: Double
, phoneType :: T.Text
} deriving (Show)
instance FromJSON Phone where
parseJSON = withObject "phone" $ \o -> Phone <$> o .: "duration" <*> o .: "phone"
-- transcript :: Transcript
-- transcript = case unsafePerformIO (decodeFileStrict "voice_transcript.json") of
-- Nothing -> error "bad json"
-- Just t -> t
transcript :: Transcript
transcript = fakeTranscript
"Testing transcript.\n\n\
\Second paragraph"
data Token = TokenWord Int Int T.Text | TokenComma | TokenPeriod | TokenParagraph
deriving (Show)
lexText :: T.Text -> [Token]
lexText = worker 0
where
worker offset txt = case T.uncons txt of
Nothing -> []
Just (c, cs)
| isSpace c
-> let (w, rest) = T.span (=='\n') txt in
if T.length w >= 2
then TokenParagraph : worker (offset + T.length w) rest
else worker (offset+1) cs
| c == '.'
-> TokenPeriod : worker (offset + 1) cs
| c == ','
-> TokenComma : worker (offset + 1) cs
| isAlphaNum c
-> let (w, rest) = T.span isAlphaNum txt
newOffset = offset + T.length w
in TokenWord offset newOffset w : worker newOffset rest
| otherwise
-> worker (offset + 1) cs
fakeTranscript :: T.Text -> Transcript
fakeTranscript input = Transcript { transcriptText = input
, transcriptWords = worker 0 (lexText input)
}
where
worker now [] = []
worker now (token : rest) = case token of
TokenWord start end w ->
let duration = 60 / wpm
in TWord { wordAligned = T.toLower w
, wordCase = "success"
, wordStart = now
, wordStartOffset = 0
, wordEnd = now + duration
, wordEndOffset = 0
, wordPhones = []
, wordReference = w
}
: worker (now + duration) rest
TokenComma -> worker (now + commaPause) rest
TokenPeriod -> worker (now + periodPause) rest
TokenParagraph -> worker (now + paragraphPause) rest
wpm = 130
paragraphPause = 0.1
commaPause = 0.05
periodPause = 0.05
-- tweenVar :: Var s a -> Duration -> (a -> Time -> a) -> Scene s ()
-- interpolateRGBA8 :: ColorComponents -> PixelRGBA8 -> PixelRGBA8 -> (Double -> PixelRGBA8)
toColor :: String -> PixelRGBA8
toColor c = case mkColor c of
ColorRef pixel -> pixel
main :: IO ()
main = reanimate $ sceneAnimation $ do
newSpriteSVG_ $ mkBackground "black"
let maskSVG =
simplify $ withStrokeWidth 0 $ lowerTransformations $ center $ latex "Masked"
-- newSpriteSVG $ maskSVG
-- newSpriteA $ animate $ \t -> mkGroup
-- [ maskedIn t maskSVG (withFillColor "blue" $ mkRect (svgWidth maskSVG) screenHeight)
-- , maskedOut t maskSVG (withFillColor "white" $ mkRect (svgWidth maskSVG) screenHeight)
-- ]
newSpriteSVG $ withFillColor "grey" $ scale 0.8 $ center $ latex $ transcriptText
transcript
-- wait 10
-- _ <- newSpriteSVG $ withStrokeWidth (defaultStrokeWidth*0.5) $
-- withStrokeColor "white" $ mkLine (-screenWidth, 0) (screenWidth, 0)
-- _ <- newSpriteSVG $ withStrokeWidth (defaultStrokeWidth*0.5) $
-- withStrokeColor "white" $ mkLine (0, -screenHeight) (0,screenHeight)
cOpacity <- newVar 0
cFill <- newVar $ toColor "black"
let
pushFill newColor = do
prevColor <- readVar cFill
tweenVar cFill 0.2
$ \p t -> if t > 0
then interpolateRGBA8 labComponents prevColor (toColor newColor) t
else p
c <- newSprite $ do
o <- unVar cOpacity
f <- unVar cFill
pure
$ withGroupOpacity o
$ withStrokeColor "white"
$ withFillColorPixel f
$ translate 4 0
$ mkCircle 1
txtVar <- newVar (mkGroup [])
t <- newSprite $ do
screenText <- unVar txtVar
pure $ withFillColor "white" $ screenText
-- waitOn $ forM_ (transcriptWords transcript) $ \TWord {..} ->
-- fork $ when (wordCase == "success") $ do
-- wait (wordStart - 0.1)
-- when (wordReference == "circle") $ do
-- wait (-0.1)
-- tweenVar cOpacity 0.2 $ \v -> fromToS v 1
-- when (wordReference == "red") $ pushFill "red"
-- when (wordReference == "green") $ pushFill "green"
-- when (wordReference == "blue") $ pushFill "blue"
-- when (wordReference == "Cyan") $ pushFill "cyan"
-- when (wordReference == "Purple") $ pushFill "purple"
-- when (wordReference == "White") $ pushFill "white"
-- writeVar txtVar $ alignText $ forceLayout wordReference
wait 3
return ()
maskedIn :: Double -> SVG -> SVG -> SVG
maskedIn t maskSVG targetSVG = mkGroup
[ mkClipPath label $ removeGroups maskSVG
, withClipPathRef (Ref label) $ translate (-svgWidth maskSVG * (1 - t)) 0 targetSVG
]
where label = "word-mask-" ++ show (hash $ renderTree maskSVG)
maskedOut :: Double -> SVG -> SVG -> SVG
maskedOut t maskSVG targetSVG = mkGroup
[ mkClipPath label $ removeGroups maskSVG
, withClipPathRef (Ref label) $ translate (svgWidth maskSVG * t) 0 targetSVG
]
where label = "word-mask-" ++ show (hash $ renderTree maskSVG)
forceLayout txt =
fst $ splitGlyphs [0, 1, 2, 3] (latex $ "\\fbox{\\phantom{TyhILW}}" <> txt)
alignText :: SVG -> SVG
alignText txt = translate 0 (svgHeight ref / 2) $ centerX txt
where ref = latex "\\fbox{Thy}"
-- abc, width=14.92 height=7.02
-- Thy, width=32.73 height=8.95