functional reactive animation mark stobbe alesya sheremet based on the work by conal elliott of...
TRANSCRIPT
Functional Reactive ANimation
Mark StobbeAlesya Sheremet
Based on the work by Conal Elliott of Microsoft Research
What is Fran?
• Haskell library for interactive animations with 2D and 3D graphics and sound
• Behaviors (animation) and events (reactive)
• DSEL designed for describing what an animation is (and not how to present it)
• Created by Conal Elliott at Microsoft Research, 1997
• Project has been suspended, last version works under Hugs98 (2002)
• Related work: Pan# and Yampa
Behaviors
One of the basic concepts in Fran:
Behavior a :: Time -> a
Type is not restricted to numbers only
Behavior Example
wiggle :: Behavior Double
wiggle = sin (pi * time)
ball :: ImageB
ball = stretch 0.2 circle
bball, rball :: ImageB
bball = moveXY 0 wiggle (withColor blue ball)
rball = moveXY wiggle 0 (withColor red ball)
main = display $ bball `over` rball
Place an image `over` the other
Overloading lets us use these operators like we are
used to
Delaying Behaviors
We can slow down animation without touching existing code, simply by transforming time
main = display $ bball `over` later 0.5 rball
main = display $ bball `over` later (time/2.0) rball
main = display $ bball `over` later (wiggle/2.0) rball
Animation 1 Animation 2 Animation 3
Time flows like a river…
delayImgs :: Behavior Double -> [ImageB] -> ImageB
delayImgs dt imgs = overs (zipWith later [0, dt ..] imgs)
trailWords :: Vector2B -> String -> ImageB
trailWords vec str = delayImgs 0.3 imgWords
where imgWords = map (moveWord vec) (words str)
moveWord :: Vector2B -> String -> ImageB
moveWord vec word = move vec (stringIm word)
main :: IO ()
main = displayU $ \u -> trailWords (mouseMotion u) trailText
where trailText = “Time flows like a river"
overs :: [ImageB] -> ImageBovers = foldl1 over
Reacting to Events Behaviors are continuous, but sometimes we should
react to discrete events Conceptually, events are Maybe a-behaviors Implemented as a separate type
Event a is a stream of event occurrences associated with a time and value of type a
newtype Event a = Event [(Time, Maybe a)]
Core of Fran’s Reactivity
newtype Event a = Event [(Time, Maybe a)]
(==>) :: Event a -> (a -> b) -> Event b(-=>) :: Event a -> b -> Event buntilB :: Behavior a -> Event (Behavior a) -> Behavior a
Event Transformers
cycle u = withColor(cycle green yellow red u) (stretch (wiggleRange 0.5 1)
circle )
where cycle c1 c2 c3 u = c1 `untilB` lbp u ==> cycle c2 c3 c1
Multiple Events
(.|.) :: Event a -> Event a -> Event a
updown n u = n `untilB` ( lbp u ==> updown (n+1) .|. rbp u ==> updown (n-1) )anim = displayU $ \u -> stretch (0.3*updown 3 u) circle
We can combine events, to wait for whichever happens first
Snapshots
A snapshot captures the value of a behavior at an event occurrence.
snapshot :: Event a -> Behavior b -> Event (a,b)snapshot_ :: Event a -> Behavior b -> Event bwhenE :: Event a -> BoolB -> Event a
Switchers and Steppers
anim u = withColor c circle where c = switcher red (lbp u -=> blue .|. rbp u -=> red)
mouseEvs u = lbp u `snapshot_` mouseMotion u
anim u = withColor blue $ move (stepper 0 (mouseEvs u)) $ stretch 0.25 circle
stepper :: a -> Event a -> Behavior aswitcher :: Behavior a -> Event (Behavior a) -> Behavior a
Demo: disappearing circles
circ :: User -> (ColorB,RealB,RealB) -> ImageBcirc u (c,d,s) = moveXY x y (stretch 0.2 (withColor c circle)) where position :: Point2B position = point2XY x y x = wiggle y = (switcher (sin(pi*time*s - d)) disappear)
-- A ball disappears when the left button of the mouse is -- pressed within 0.2 of the centre of the ball.
disappear :: Event RealBdisappear = whenSnap (lbp u) apart (\ x p -> (p <= 0.2)) -=> -2 where apart = distance2 (mouse u) position
Demo: grab and followfollowMouse u p0 = (pos, closeEnough) where
pos, lastRelease :: Point2Bpos = ifB grabbing (mouse u) lastReleaselastRelease = stepper p0 (release `snapshot_` pos)
closeEnough, grabbing :: BoolBcloseEnough = distance2 pos (mouse u) <* grabDistancegrabbing = stepper False (grab -=> True .|. release -=> False)
grab, release :: Event ()grab = lbp u `whenE` closeEnoughrelease = lbr u `whenE` grabbing
grabDistance :: RealBgrabDistance = 0.1
Predicate
Behavior Event
predicate
stepper
predicate :: BoolB -> User -> Event()
stepper :: a -> Event a -> Behavior a
Modelling a bouncing ball game
We are going to model a very simple game:
Pong
But we limit our self to a basic variant:
• No randomization
• Only one player
Modelling a bouncing ball game
-- constants (all type Double)
playfield_size = 2.0 paddle_height = 0.1
ball_radius = 0.05 paddle_width = 0.3
ball_dx0 = 0.2 paddle_velocity = 1.0
ball_dy0 = 0.4
-- playfield, ball and paddle (all type ImageB)
playfield = withColor blue (stretch (constantB playfield_size) square)
ball = withColor yellow (stretch (constantB ball_radius) circle)
paddle =
let paddle’ = withColor green (rect (constantB paddle_width)
(constantB paddle_height))
in moveXY 0 (constantB (-(playfield_size / 2.0))) paddle’
Modelling a bouncing ball game (2)
main :: IO ()
main = displayU $ \u ->
let (px,py) = movePaddle u
(bx,by) = bounceOnPaddle ball_dx0 ball_dy0 px u
paddle' = moveXY px py paddle
ball' = moveXY bx by ball
in paddle' `over` ball' `over` playfield
Modelling a bouncing ball game (3)movePaddle :: User -> (Behavior Double, Behavior Double)
movePaddle u =
let ux = stepper 0 ( rightKey u -=> paddle_velocity
.|. leftKey u -=> -(paddle_velocity)
.|. anyKey u -=> 0
)
dx = condB (hitLeft &&* ux <* 0 ||* hitRight &&* ux >* 0) 0 ux
-- test if we cross a border
hitLeft = x <* (constantB ((paddle_width/2) - playfield_size))
hitRight = x >* (constantB (playfield_size - (paddle_width/2)))
-- return (x,y) integrated over time
x = integral dx u
y = constantB 0
in (x, y)
Modelling a bouncing ball game (4)
bounceOnPaddle :: Double -> Double -> Behavior Double ->
User -> (Behavior Double, Behavior Double)
bounceOnPaddle dx0 dy0 px u =
let x = integral dx u
y = integral dy u
-- check if the ball is about to cross a border
bLeft = x <* (constantB (ball_radius-playfield_size))
bRight = x >* (constantB (playfield_size-ball_radius))
bBottom = y <* (constantB (ball_radius-playfield_size+paddle_height))
bTop = y >* (constantB (playfield_size-ball_radius))
outside = bLeft ||* bRight ||* bBottom ||* bTop
-- bounce against border, if allowed
dx = bounce dx0 dx doHBounce
dy = bounce dy0 dy doVBounce
Modelling a bouncing ball game (5)
Why not only keep the last border we bounced against?
We can’t get out the corner!
Modelling a bouncing ball game (6) -- what was the last bounce direction (vertical or horizontal)
lastBounceLR = switcher (negOrPos dx0) ( predicate bRight u -=> 1
.|. predicate bLeft u -=> (-1))
lastBounceTB = switcher (negOrPos dy0) ( predicate bTop u -=> 1
.|. predicate bBottom u -=> (-1))
-- is bouncing allowed (vertical or horizontal)
doVBounce = predicate ((lastBounceTB ==* (-1) &&* bTop)
||* (lastBounceTB ==* 1 &&* bBottom
&&* doPBounce)) u
doHBounce = predicate ((lastBounceLR ==* (-1) &&* bRight)
||* (lastBounceLR ==* 1 &&* bLeft)) u
-- paddle in the right place?
doPBounce = x <* (px + (constantB paddle_width / 2))
&&* x >* (px - (constantB paddle_width / 2))
in (x,y)
Modelling a bouncing ball game (7)
-- calculate initial direction
negOrPos :: Double -> Behavior Double
negOrPos x = if (x < 0) then (constantB 1) else (constantB (-1))
-- bounce the ball by negating the direction
bounce :: Double -> Behavior Double -> Event a -> Behavior Double
bounce dz0 dz doBounce = stepper dz0 (doBounce `snapshot_` dz ==> negate)
Some problems:• imprecision• missing state (not in the example)
Conclusion Modeling approach to animation Events and Behaviors are first class values Behaviors are used for
Describing what, not how, something should vary over time Not limited to numbers
Event-oriented programming allows Remember events (snapshots and steppers) Enrich events with (-=>) and (.|.)
Difficult to “get it right” Speed is ok, but successors like Yampa perform better