worldturtle-0.2.2.0: Graphics/WorldTurtle/Internal/Sequence.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BangPatterns #-}
{-# OPTIONS_HADDOCK hide #-}
module Graphics.WorldTurtle.Internal.Sequence
( Turtle
, TSC
, SequenceCommand
, defaultTSC
, processTurtle
, renderTurtle
, addPicture
, simTime
, setSimTime
, decrementSimTime
, pics
, totalSimTime
, turtles
, generateTurtle
, animate'
, animate
, combineSequence
, alternateSequence
) where
import Graphics.WorldTurtle.Internal.Turtle
import Graphics.Gloss.Data.Picture (Picture)
import Control.Applicative (empty)
import Control.Monad (when)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.State.Strict
import Control.Lens
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
-- | State Monad that takes our `TSC` type as its state object.
type TurtleState = State TSC
-- | Maybe Monad on top of the State Monad of form @SequenceCommand a@.
-- This represents a computation that can be "partial." I.E. we can only
-- animate so much of the scene with the time given.
type SequenceCommand a = MaybeT TurtleState a
-- Careful of editing the Turtle comment below as it is public docs!
-- Really "Turtle" is just a handle to internal TurtleData. It is a key that
-- looks up TurtleData in a map. Since Turtle is exposed to the user-level we
-- do not document it in this way however.
-- | The Turtle that is drawn on the canvas! Create a new turtle using
-- `Graphics.WorldTurtle.Commands.makeTurtle`.
newtype Turtle = Turtle Int deriving (Eq, Ord)
data TSC = TSC
{ _pics :: ![Picture] -- ^ All pictures that make up the current canvas
, _totalSimTime :: !Float -- ^ Remaining available for animating
, _turtles :: !(Map Turtle TurtleData) -- Collection of all turtles.
, _nextTurtleId :: !Int -- ^ ID of next turtle to be generated.
}
$(makeLenses ''TSC)
-- | Generates default parameter arguments.
defaultTSC :: Float -> TSC
defaultTSC givenTime = TSC
{ _pics = mempty
, _totalSimTime = givenTime
, _turtles = Map.empty
, _nextTurtleId = 0
}
-- | Gets the remaining simulation time of the current turtle process.
-- The simulation time dictates how much time is remaining for an animation,
-- and it will be reduced as the animations play in sequence. Once this value
-- hits 0 the exit command will be called and the monad will stop processing.
simTime :: SequenceCommand Float
simTime = use totalSimTime
-- | Sets the simulation time in the state monad.
-- If the simulation time is <= 0 then this setter will immediately call the
-- exit function which will kill any further processing of the monad.
setSimTime :: Float -- ^ Time to set.
-> SequenceCommand ()
setSimTime newTime = do
let newTime' = max 0 newTime
totalSimTime .= newTime'
when (newTime' <= 0) empty
-- | Takes a value away form the current sim time and store the updated time.
-- See `setSimTime`.
decrementSimTime :: Float -- ^ Value to subtract from store simulation time.
-> SequenceCommand ()
decrementSimTime duration = simTime >>= \ t -> setSimTime (t - duration)
-- | Given a picture, adds it to the picture list.
addPicture :: Picture -- ^ Picture to add to our animation
-> SequenceCommand ()
addPicture p = pics %= ($!) (p :)
-- | Given a sequence and a State, returns the result of the computation and the
-- final state of the computation of form @(r, s)@. When @r@ is @Just@, then
-- the computation completed, otherwise the computation ended early due to
-- lack of time available (i.e. a partial animation).
processTurtle :: SequenceCommand a
-> TSC
-> (Maybe a, TSC)
processTurtle commands tsc =
let drawS = runMaybeT $ decrementSimTime 0 >> commands
in runState drawS tsc
-- | Given a computation to run and an amount of time to run it in, renders the
-- final "picture".
renderTurtle :: SequenceCommand a
-> Float
-> Picture
renderTurtle c f = let (_, s) = processTurtle c t
t = defaultTSC f
in mconcat $ reverse (s ^. pics) ++ drawTurtles (s ^. turtles)
drawTurtles :: Map Turtle TurtleData -> [Picture]
drawTurtles m = drawTurtle <$> Map.elems m
generateTurtle :: SequenceCommand Turtle
generateTurtle = do
t <- Turtle <$> use nextTurtleId
turtles %= Map.insert t defaultTurtle
nextTurtleId += 1
return t
animate' :: Float
-> Float
-> (Float -> SequenceCommand a)
-> SequenceCommand a
animate' distance turtleSpeed callback =
let duration = distance / turtleSpeed
d' = if isNaN duration || isInfinite duration then 0 else duration
-- if speed is 0 we use this as a "no animation" command from
-- user-space.
in animate (abs d') callback
animate :: Float
-> (Float -> SequenceCommand a)
-> SequenceCommand a
animate duration callback = do
timeRemaining <- simTime -- simulation time to go
let availableTime = min timeRemaining duration
-- Amount of time we have to complete the animation before we need to exit.
let timeQuot = if availableTime == 0 then 1 else availableTime / duration
-- quotient of available time vs required time. Note that when the duration
-- is 0 we say "don't do any animation"
t <- callback timeQuot
-- Perform the calculation with the quotient for lerping
decrementSimTime availableTime
-- Test to see if this is the end of our animation and if so exit early
return t
-- | Runs two items in parallel then applies a semigroup combination operator
-- to the result of both.
-- This combination can only return if both A and B return. Compare to
-- `alternateSequence` which can return if one returns.
combineSequence :: Semigroup a
=> SequenceCommand a -- ^ Sequence /a/ to run.
-> SequenceCommand a -- ^ Sequence /b/ to run.
-> SequenceCommand a
-- ^ New sequence of A and B in parallel.
combineSequence a b = do
(aVal, bVal) <- runParallel a b
combo aVal bVal
where combo (Just x) (Just y) = return (x <> y)
combo _ _ = empty
-- | Runs two items in sequence, returns the result of /a/ if /a/ passes,
-- otherwise returns the results of /b/. The implication of this is that only
-- the result of a will be returned while animating, and b when animation is
-- finished.
alternateSequence :: SequenceCommand a -- ^ Sequence /a/ to run.
-> SequenceCommand a -- ^ Sequence /b/ to run.
-> SequenceCommand a
alternateSequence a b = do
(aVal, bVal) <- runParallel a b
combo aVal bVal
where combo (Just x) _ = return x
combo _ (Just y) = return y
combo _ _ = empty
-- | Given two sequences /a/ and /b/, instead of running them both as separate
-- animations, run them both in parallel!
runParallel :: SequenceCommand a -- ^ Sequence /a/ to run.
-> SequenceCommand b -- ^ Sequence /b/ to run.
-> SequenceCommand (Maybe a, Maybe b)
-- ^ New sequence of A and B which returns both results.
runParallel a b = do
startSimTime <- use totalSimTime
s <- lift get
-- Run the "A" animation
let (aVal, s') = processTurtle a s
let aSimTime = s' ^. totalSimTime
-- Run the "B" animation from the same time
let (bVal, s'') = processTurtle b $ s' & totalSimTime .~ startSimTime
-- No subsequent animation can proceed until the longest animation completes.
-- We take the remaining animation time to be the remaining time of the
-- longest running animation
lift $ put $ s'' & totalSimTime %~ min aSimTime
-- Now we must test the remaining sim time. The above calls might have
-- succeeded while still exhausting our remaining time -- which as far as
-- animating is concerned is the same as not succeeding at all!
decrementSimTime 0
return (aVal, bVal)