packages feed

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)