packages feed

yampa-canvas-0.2.2: example/BouncingBalls.hs

{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}

module BouncingBalls where

-- set browser to: http://localhost:3000/
import Graphics.Blank hiding (Event)
import FRP.Yampa
import FRP.Yampa.Vector2
import Data.Text(Text)

import FRP.Yampa.Canvas

---------------------------------------------------

main :: IO ()
main = blankCanvas 3000 animateBouncingBalls

-- | Display an animation of multiple falling balls.
animateBouncingBalls :: DeviceContext -> IO ()
animateBouncingBalls = reactimateSFinContext (\ _ -> return NoEvent) renderScene (bouncingBalls someBalls)

---------------------------------------------------

type Radius = Double

type Acceleration = Vector2 Double
type Velocity     = Vector2 Double
type Position     = Vector2 Double

type Colour = Text

---------------------------------------------------

data Ball = MkBall { pos    :: Position,
                     vel    :: Velocity,
                     radius :: Radius,
                     colour :: Colour
                   }

negateXVel :: Ball -> Ball
negateXVel b = b { vel = let (x,y) = vector2XY (vel b)
                          in vector2 (negate x) y
                 }

negateYVel :: Ball -> Ball
negateYVel b = b { vel = let (x,y) = vector2XY (vel b)
                          in vector2 x (negate y)
                 }

---------------------------------------------------

ball1 :: Ball
ball1 = MkBall { pos    = vector2 0.1 0.5,
                 vel    = vector2 0.4 0.1,
                 radius = 0.05,
                 colour = "red"
               }

ball2 :: Ball
ball2 = MkBall { pos    = vector2 0.3 0.7,
                 vel    = vector2 (-0.5) (-0.1),
                 radius = 0.03,
                 colour = "blue"
               }

ball3 :: Ball
ball3 = MkBall { pos    = vector2 0.5 0.4,
                 vel    = vector2 0.2 1.3,
                 radius = 0.04,
                 colour = "green"
               }

ball4 :: Ball
ball4 = MkBall { pos    = vector2 0.7 0.8,
                 vel    = vector2 (-0.1) (-0.2),
                 radius = 0.06,
                 colour = "yellow"
               }

ball5 :: Ball
ball5 = MkBall { pos    = vector2 0.2 0.9,
                 vel    = vector2 1.8 0.85,
                 radius = 0.02,
                 colour = "orange"
               }

someBalls :: [Ball]
someBalls = [ball1,ball2,ball3,ball4,ball5]

gravity :: Acceleration
gravity = vector2 0 (-1)

---------------------------------------------------

-- | Construct a free-falling ball from an initial ball configuration.
fallingBall :: Ball -> SF x Ball
fallingBall b = constant gravity >>> accelerator (vel b) (pos b) >>> arr updateBall
  where
    updateBall :: (Position,Velocity) -> Ball
    updateBall (p,v) = b { pos = p, vel = v }

-- | Construct a ball that bounces in one dimension from an initial ball configuration.
bouncingBall1d :: forall x. Ball -> SF x Ball
bouncingBall1d b = switchWhen (fallingBall b) detectFloor f
  where
    detectFloor :: SF Ball (Event Ball)
    detectFloor = edgeWhen p
      where
        p :: Ball -> Bool
        p b1 = (vector2Y (pos b1) <= radius b1) || (vector2Y (pos b1) >= 1 - radius b1)

    f :: Ball -> SF x Ball
    f b2 = bouncingBall1d (negateYVel b2)

-- | Construct a ball that bounces in two dimensions.
bouncingBall2d :: forall x. Ball -> SF x Ball
bouncingBall2d b = switchWhen (bouncingBall1d b) detectWall f
  where
    detectWall :: SF Ball (Event Ball)
    detectWall = edgeWhen p
      where
        p :: Ball -> Bool
        p b1 = (vector2X (pos b1) <= radius b1) || (vector2X (pos b1) >= 1 - radius b1)

    f :: Ball -> SF x Ball
    f b2 = bouncingBall2d (negateXVel b2)

-- | Construct a list of bouncing balls from a list of initial ball configurations.
bouncingBalls :: [Ball] -> SF x [Ball]
bouncingBalls bs = parB (map bouncingBall2d bs)

-------------------------------------------------------------------

-- | Draw a circle.
circle :: Position -> Radius -> Colour -> Canvas ()
circle pos' r col = do beginPath ()
                       arc (vector2X pos', vector2Y pos', r, 0, pi*2, False)
                       closePath ()
                       fillStyle col
                       fill ()

-- | A Canvas action to render a single Ball.
renderBall :: Ball -> Canvas ()
renderBall b = circle (pos b) (radius b) (colour b)

-- | A Canvas action to render a list of Balls.
renderBalls :: [Ball] -> Canvas ()
renderBalls = mapM_ renderBall

-- | A Canvas action to render the entire scene.
renderScene :: [Ball] -> Canvas ()
renderScene bs = scaleScene >> renderBalls bs

-- | We scale such that (0,0) is the bottom-left of the canvas and (1,1) is the top-right.
scaleScene :: Canvas ()
scaleScene =
   do context <- myCanvasContext
      let w = width context
          h = height context
      translate (0,h)
      scale (w, negate h)

-------------------------------------------------------------------

-- Auxillary signal function combinators not provided by the Yampa library.

-- | Given an initial 'Velocity' and 'Position', produce a signal function that takes an acceleration and emits
--   the current 'Velocity' and 'Distance'.
accelerator :: Velocity -> Position -> SF Acceleration (Position,Velocity)
accelerator v0 d0 = imIntegral v0 >>> (imIntegral d0 &&& identity)

-- | A variant of 'FRP.Yampa.switch' where the event only depends on the output of the sub-ordinate signal function.
switchWhen :: SF a b -> SF b (Event e) -> (e -> SF a b) -> SF a b
switchWhen sf1 sfe = switch (sf1 >>> identity &&& sfe)

-- | A variant of 'FRP.Yampa.edge' which takes a predicate on an input signal, and tags the event with the value of the input signal.
edgeWhen :: (a -> Bool) -> SF a (Event a)
edgeWhen p = (p ^>> edge) &&& identity >>^ uncurry tag

-------------------------------------------------------------------