packages feed

worldturtle-0.1.0.0: Graphics/WorldTurtle/Commands.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE BangPatterns #-}
{-|
Module      : Graphics.WorldTurtle.Commands
Description : The commands used 
Copyright   : (c) Archibald Neil MacDonald, 2020
License     : BSD3
Maintainer  : FortOyer@hotmail.co.uk
Stability   : experimental
Portability : POSIX

This module is a collection of all the commands used to manipulate a turtle!

-}
module Graphics.WorldTurtle.Commands
  (
  -- * Setting up a turtle.
    Turtle
  , makeTurtle
  , makeTurtle'
  -- * Movement commands.
  , P.Point
  , forward
  , fd
  , backward
  , bk
  , left
  , lt
  , right
  , rt
  , Graphics.WorldTurtle.Commands.circle
  , goto
  , setPosition
  , home
  , setHeading
  , setSpeed
  -- * Styling commands.
  , stamp
  , representation
  -- * Tell turtle's state.
  , position
  , heading
  , speed
  , penColor
  , penDown
  , penSize
  , visible
  -- * Drawing state.
  , setPenColor
  , setPenDown
  , setPenSize
  , setRepresentation
  , setVisible
  -- * Canvas commands.
  , clear
  -- * Common constants
  , east
  , north
  , west
  , south
  ) where

import Data.Maybe (fromMaybe)

import Control.Lens
import Control.Monad

import Graphics.WorldTurtle.Shapes

import Graphics.WorldTurtle.Internal.Commands
import Graphics.WorldTurtle.Internal.Sequence

import qualified Graphics.WorldTurtle.Internal.Turtle as T
import qualified Graphics.WorldTurtle.Internal.Coords as P

import Graphics.Gloss.Data.Color (Color, black)

import Graphics.Gloss.Data.Picture

{- |
Creates a new `Turtle` and displays it on the canvas. This turtle can then be
manipulated! For example, to create a turtle and then move the turtle forward:

   @
    main:: IO ()
    main = runTurtle $ do
      t <- makeTurtle
      forward 90 t
   @

The default turtle starts at position (0, 0) and is orientated @North@.

-}
makeTurtle :: TurtleCommand Turtle
makeTurtle = TurtleCommand generateTurtle

{-| This variant of `makeTurtle` takes a starting position, a starting 
    orientation, and a color to apply to the turtle and the turtle's pen.

    @
      myCommand :: TurtleCommand ()
      myCommand = do
        t1 <- makeTurtle' (0, 0)  0 green
        t2 <- makeTurtle' (0, 0) 90 red
        forward 90 t1 \<|\> forward 90 t2
    @

    See `makeTurtle`.
-}
makeTurtle' :: Point -- ^ Initial position of the turtle.
            -> Float -- ^ Initial heading of the turtle.
            -> Color -- ^ Color of the turtle and the turtle's pen.
            -> TurtleCommand Turtle -- ^ The generated turtle.
makeTurtle' p f c = TurtleCommand $ do 
  turtle <- generateTurtle
  let ts = turtLens_ turtle
  ts . T.position       .= p
  ts . T.heading        .= f
  ts . T.representation .= turtleArrow black c
  ts . T.penColor       .= c
  return turtle

-- | Move the turtle backward by the specified @distance@, in the direction the 
--   turtle is headed.
backward :: Float -- ^ Distance to move the turtle.
         -> Turtle -- ^ The turtle to move.
         -> TurtleCommand ()
backward !d = forward (-d)

-- | Shorthand for `backward`.
bk :: Float -> Turtle -> TurtleCommand ()
bk = backward

-- | Turn a turtle right by the given degrees amount.
right :: Float -- ^ Rotation amount to apply to turtle.
      -> Turtle -- ^ The turtle to rotate.
      -> TurtleCommand ()
right !r = left (-r)

-- | Shorthand for `right`.
rt :: Float -> Turtle -> TurtleCommand ()
rt = right

calculateNewPointF_ :: P.Point -- ^ Starting point
                    -> Float -- ^ Distance
                    -> Float -- ^ Heading in degrees.
                    -> Float -- ^ coefficient [0, 1]
                    -> P.Point
calculateNewPointF_ !p !d !h !q = P.lerp q p endP
  where !vec = P.rotateV (P.degToRad h) (d, 0)
        !endP = vec P.+ p

-- | Move the turtle forward by the specified @distance@, in the direction the 
--   turtle is headed.
forward :: Float -- ^ Distance to move the turtle.
        -> Turtle -- ^ The turtle to move.
        -> TurtleCommand ()
forward !d turtle = TurtleCommand $ do
    t <- tData_ turtle
    --  Get origin point
    animate' d (t ^. T.speed) $ \ q -> do
      --  Get new endpoint via percentage
      let !startP = t ^. T.position
      let !midP = calculateNewPointF_ startP d (t ^. T.heading) q
      when (t ^. T.penDown) $ do -- don't draw if pen isn't in down state
        addPicture $ color (t ^. T.penColor) 
                   $ thickLine startP midP (t ^. T.penSize)
        --  Draw line from startPoint to midPoint.
      turtLens_ turtle . T.position .= midP
      --  Update the turtle to a new position

-- | Shorthand for `forward`.
fd :: Float -> Turtle -> TurtleCommand ()
fd = forward

-- | Stamp a copy of the turtle shape onto the canvas at the current turtle 
--   position.
stamp :: Turtle -- ^ The turtle with the shape to be copied.
      -> TurtleCommand ()
stamp turtle = TurtleCommand $ tData_ turtle >>= addPicture . T.drawTurtle 

-- | Turn a turtle left by the given degrees amount.
left :: Float -- ^ Rotation amount to apply to turtle.
     -> Turtle -- ^ The turtle to rotate.
     -> TurtleCommand ()
left !r turtle = TurtleCommand $ do
    t <- tData_ turtle
    let r' = P.normalizeDirection r
    animate' (P.degToRad r') (t ^. T.speed) $ \q -> do
      let !h = t ^. T.heading
      --let q' = if r > 0 then q else -q
      let !newHeading = P.normalizeHeading $ h + q * r'
      --  Get new heading via percentage
      turtLens_ turtle . T.heading .= newHeading

-- | Shorthand for `left`.
lt :: Float -> Turtle -> TurtleCommand ()
lt = left

-- | Draws an arc starting from a given starting point on the edge of the
--   circle.
drawCircle_ :: P.Point -- ^ Point on edge of circle to start from
            -> Float -- ^ Radius of circle
            -> Float -- ^ Absolute starting angle in degrees
            -> Float -- ^ Rotation amount about radius in degrees
            -> Float -- ^ Line thickness (penSize)
            -> Color -- ^ Color of circle 
            -> Picture -- ^ Resulting circle
drawCircle_ !p !radius !startAngle !endAngle !pSize !pColor = 
 translate (fst p) (snd p) $ rotate (180 - startAngle)
                           $ translate (-radius) 0
                           $ color pColor
                           $ rotate (if radius >= 0 then 0 else 180)
                           $ thickArc 0 (endAngle) radius pSize

-- Calculates the next position of a turtle on a circle.
calculateNewPointC_ :: P.Point -- ^ Point on edge of circle
                    -> Float -- ^ Radius of circle
                    -> Float -- ^ Absolute starting angle in degrees
                    -> Float -- ^ Rotation amount about radius in degrees
                    -> P.Point -- ^ Resulting new point
calculateNewPointC_ !p !radius !startAngle !angle = (px, py)
  where !px = fst p - (radius * (cA - cS))
        !py = snd p - (radius * (sA - sS))
        !s = P.degToRad startAngle
        !a = P.degToRad $ angle + startAngle
        !cS = cos s
        !sS = sin s
        !cA = cos a
        !sA = sin a

-- | Draw an arc with a given @radius@. The center is @radius@ units left of the
--   @turtle@ if positive. Otherwise  @radius@ units right of the @turtle@ if 
--   negative.
--
--   The arc is drawn in an anticlockwise direction if the radius is positive,
--   otherwise, it is drawn in a clockwise direction.
circle  :: Float -- ^ Radius of the circle.
        -> Float -- ^ Angle to travel in degrees. 
                 -- For example: @360@ for a full circle or @180@ for a 
                 -- semicircle.
        -> Turtle -- ^ Turtle to move in a circle.
        -> TurtleCommand ()
circle !radius !r turtle = TurtleCommand $ do
  t <- tData_ turtle
  let !r' = P.normalizeHeading r
  animate' (radius * P.degToRad r') (t ^. T.speed) $ \ q -> do
    let !startAngle = t ^. T.heading + 90
    let !p = t ^. T.position
    let !angle = r' * q
    when (t ^. T.penDown) $ do -- don't draw if pen isn't in down state
      addPicture $! drawCircle_ p radius startAngle angle 
                    (t ^. T.penSize) (t ^. T.penColor)

    -- Update the turtle with the new values.
    let ts = turtLens_ turtle
    ts . T.heading .= (P.normalizeHeading $ startAngle - 90 + angle)

    let !p' = calculateNewPointC_ p radius startAngle angle
    ts . T.position .= p'

-- | Returns the turtle's current position.
--   Default (starting) position is @(0, 0)@.
position :: Turtle -- ^ Turtle to query.
         -> TurtleCommand P.Point -- ^ Returned current point.
position = getter_ (0, 0) T.position

-- | Warps the turtle to its starting position @(0, 0)@ and resets the
-- orientation to @North@ (90 degrees). No line is drawn moving the turtle.
home :: Turtle
     -> TurtleCommand ()
home turtle = TurtleCommand $ do
  let ts = turtLens_ turtle
  ts . T.position       .= (0, 0)
  ts . T.heading        .= 90

-- | Warps the turtle to a new position.
--   The turtle jumps to this new position with no animation. If the pen is down
--   then a line is drawn.
goto :: P.Point -- ^ Position to warp to.
     -> Turtle -- ^ Turtle to modify.
     -> TurtleCommand ()
goto point turtle = TurtleCommand $ do
  t <- tData_ turtle
  let startP = t ^. T.position
  when (t ^. T.penDown) $ addPicture 
                        $ color (t ^. T.penColor) 
                        $ thickLine startP point (t ^. T.penSize)
  turtLens_ turtle . T.position .= point

-- | Alias of `goto`.
setPosition :: P.Point -> Turtle -> TurtleCommand ()
setPosition = goto

-- | Returns the turtle's heading.
--   
--   @0@ is along the positive x-axis, going anticlockwise. So:
--
--   * East is @0@ degrees.
--   * North is @90@ degrees.
--   * West is @180@ degrees.
--   * South is @270@ degrees.
--
--   The default heading is North (@90@ degrees).
heading :: Turtle -- ^ Turtle to query.
         -> TurtleCommand Float -- ^ Returned heading as angle in degrees.
heading = getter_ 0 T.heading

-- | Sets the turtle's heading. See `heading`.
setHeading :: Float -- ^ Heading to apply. 
           -> Turtle -- ^ Turtle to set.
           -> TurtleCommand ()
setHeading = setter_ T.heading

-- | Returns the turtle's pen color.
--   The color of the turtle's pen.The default color is @black@.
penColor :: Turtle -- ^ Turtle to query.
         -> TurtleCommand Color -- ^ Returned current pen color.
penColor = getter_ black T.penColor

-- | Set the turtle's pen color.
--  See `penColor`.
setPenColor :: Color -- ^ New pen color to apply
            -> Turtle -- ^ Turtle to modify.
            -> TurtleCommand ()
setPenColor = setter_ T.penColor

-- | Returns whether the turtle's pen is down.
--   When the turtle's pen is down it will draw a line when it moves.
--   The default value is @true@.
penDown :: Turtle -- ^ Turtle to query.
         -> TurtleCommand Bool -- ^ True if pen is down, false if not.
penDown = getter_ False T.penDown

-- | Sets the turtle's pen to down or up.
--   See `penDown`.
setPenDown :: Bool -- ^ New state for pen flag. True for down. False for up.
           -> Turtle -- ^ Turtle to modify.
           -> TurtleCommand ()
setPenDown = setter_ T.penDown

-- | Returns the turtle's pen size.
--   Defaults to @2@.
penSize :: Turtle -- ^ Turtle to query.
         -> TurtleCommand Float -- ^ Size of turtle's pen.
penSize = getter_ 0 T.penSize

-- | Sets the turtle's pen size.
--   See `penSize`.
setPenSize :: Float -- ^ New size for turtle's pen.
           -> Turtle -- ^ Turtle to modify.
           -> TurtleCommand ()
setPenSize = setter_ T.penSize

-- | Returns whether the turtle is visible.
--   The default value is @true@.
visible :: Turtle -- ^ Turtle to query.
        -> TurtleCommand Bool -- ^ True if turtle is visible,false if not.
visible = getter_ False T.visible

-- | Sets the turtle's visibility.
--   See `visible`.
setVisible :: Bool -- ^ New state for visible flag.
           -> Turtle -- ^ Turtle to modify.
           -> TurtleCommand ()
setVisible = setter_ T.visible

-- | Returns whether the turtle's current speed.
--   Speed is is @distance@ per second.
--   A speed of 0 is equivalent to no animation being performed and instant 
--   drawing.
-- The default value is @200@.
speed :: Turtle -- ^ Turtle to query.
      -> TurtleCommand Float
speed = getter_ 0 T.speed

-- | Sets the turtle's speed.
--   See `speed`.
setSpeed :: Float -- ^ New speed.
         -> Turtle -- ^ Turtle to modify.
         -> TurtleCommand ()
setSpeed = setter_ T.speed

-- | Gets the turtle's representation as a Gloss `Picture`.
representation :: Turtle -- ^ Turtle to query.
               -> TurtleCommand Picture
representation = getter_ blank T.representation

{- | Sets the turtle's representation to a Gloss `Picture`.
   See `representation`.
   For example, to set the turtle as a red circle:
   
   @
    import Graphics.WorldTurtle
    import qualified Graphics.Gloss.Data.Picture as G

    myCommand :: TurtleCommand ()
    myCommand = do
      t <- makeTurtle
      setPenColor red t
      setRepresentation (G.color red $ G.circleSolid 10) t
      forward 90 t
   @
-}
setRepresentation :: Picture
                  -> Turtle -- ^ Turtle to mutate.
                  -> TurtleCommand ()
setRepresentation = setter_ T.representation

-- | Clears all drawings form the canvas. Does not alter any turtle's state.
clear :: TurtleCommand ()
clear = TurtleCommand $ pics .= []

-- | @90@ degrees.
north :: Float
north = 90

-- | @0@ degrees.
east :: Float
east = 0

-- | @180@ degrees.
west :: Float 
west = 180

-- | @270@ degrees.
south :: Float
south = 270

{-
   Here be dirty helper functions:
-}

-- | Looks up the turtle data for the given turtle in the state monad.
-- This type signature comes form GHC...my prism-foo is not good enough to sugar it.
turtLens_ :: Applicative f 
          => Turtle 
          -> (T.TurtleData -> f T.TurtleData) 
          -> TSC b 
          -> f (TSC b) 
turtLens_ t = turtles . at t . _Just
{-# INLINE turtLens_ #-}

-- | This is a helper function for our getter commands.
--   It takes a default value, the lense to compose, and the turtle to inspect.
getter_ :: a -> Lens' T.TurtleData a -> Turtle -> TurtleCommand a
getter_ def l t = 
  TurtleCommand $ fromMaybe def <$> preuse (turtLens_ t . l)
{-# INLINE getter_ #-}

-- | This is a helper function that extracts the turtle data for a given turtle.
tData_ :: Turtle -> SeqC T.TurtleData
tData_ = seqT <$> getter_ T.defaultTurtle id
{-# INLINE tData_ #-}

-- | This is a helper function for our setter commands
-- It takes a lens, the value to apply, and the turtle to modify.
setter_ :: Lens' T.TurtleData b -> b -> Turtle -> TurtleCommand ()
setter_ l val t = 
  TurtleCommand $ turtLens_ t . l .= val
{-# INLINE setter_ #-}