{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
module CodeWorld.Reflex
(
reflexOf,
ReactiveInput,
keyPress,
keyRelease,
textEntry,
pointerPress,
pointerRelease,
pointerPosition,
pointerDown,
timePassing,
reactiveOf,
debugReactiveOf,
ReflexCodeWorld,
getKeyPress,
getKeyRelease,
getTextEntry,
getPointerClick,
getPointerPosition,
isPointerDown,
getTimePassing,
draw,
Picture,
blank,
polyline,
thickPolyline,
polygon,
thickPolygon,
solidPolygon,
curve,
thickCurve,
closedCurve,
thickClosedCurve,
solidClosedCurve,
rectangle,
solidRectangle,
thickRectangle,
circle,
solidCircle,
thickCircle,
arc,
sector,
thickArc,
lettering,
TextStyle (..),
Font (..),
styledLettering,
colored,
coloured,
translated,
scaled,
dilated,
rotated,
pictures,
(<>),
(&),
coordinatePlane,
codeWorldLogo,
Point,
translatedPoint,
rotatedPoint,
scaledPoint,
dilatedPoint,
Vector,
vectorLength,
vectorDirection,
vectorSum,
vectorDifference,
scaledVector,
rotatedVector,
dotProduct,
Color (..),
Colour,
pattern RGB,
pattern HSL,
black,
white,
red,
green,
blue,
yellow,
orange,
brown,
pink,
purple,
gray,
grey,
mixed,
lighter,
light,
darker,
dark,
brighter,
bright,
duller,
dull,
translucent,
assortedColors,
hue,
saturation,
luminosity,
alpha,
)
where
import CodeWorld.Color
import CodeWorld.Driver
import CodeWorld.Picture
import Control.Monad.Fix
import Control.Monad.Trans
import Data.Bool
import qualified Data.Text as T
import Numeric (showFFloatAlt)
import Reflex
reflexOf ::
( forall t m.
( Reflex t,
MonadHold t m,
MonadFix m,
PerformEvent t m,
MonadIO (Performable m),
Adjustable t m,
PostBuild t m
) =>
ReactiveInput t ->
m (Dynamic t Picture)
) ->
IO ()
reflexOf program = runReactive $ \input -> do
pic <- program input
return (pic, pic)
{-# WARNING
reflexOf
[ "Please use reactiveOf instead of reflexOf.",
"reflexOf will be removed and replaced soon."
]
#-}
reactiveOf :: (forall t m. ReflexCodeWorld t m => m ()) -> IO ()
reactiveOf program = runReactive $ \input -> runReactiveProgram program input
{-# WARNING
reactiveOf
[ "After the current migration is complete,",
"reactiveOf will probably be renamed to reflexOf."
]
#-}
debugReactiveOf :: (forall t m. ReflexCodeWorld t m => m ()) -> IO ()
debugReactiveOf program = runReactive $ \input -> flip runReactiveProgram input $ do
hoverAlpha <- getHoverAlpha
controlState <- reactiveDebugControls hoverAlpha
logicalInputs <- makeLogicalInputs controlState =<< getReactiveInput
withReactiveInput logicalInputs program
data ControlState t
= ControlState
{ csRunning :: Dynamic t Bool,
csTimeDilation :: Dynamic t Double,
csPointTransform :: Dynamic t (Point -> Point),
csSyntheticStep :: Event t ()
}
makeLogicalInputs :: (Reflex t, MonadHold t m) => ControlState t -> ReactiveInput t -> m (ReactiveInput t)
makeLogicalInputs ControlState {..} input = do
keyPress <- return $ gateDyn csRunning $ keyPress input
keyRelease <- return $ gateDyn csRunning $ keyRelease input
textEntry <- return $ gateDyn csRunning $ textEntry input
pointerPress <- return $ gateDyn csRunning $ attachWith ($) (current csPointTransform) (pointerPress input)
pointerRelease <- return $ gateDyn csRunning $ attachWith ($) (current csPointTransform) (pointerRelease input)
pointerPosition <- freezeDyn csRunning $ csPointTransform <*> pointerPosition input
pointerDown <- freezeDyn csRunning $ pointerDown input
timePassing <-
return $
mergeWith
(+)
[ gateDyn csRunning $ attachWith (*) (current csTimeDilation) (timePassing input),
0.1 <$ csSyntheticStep
]
return ReactiveInput {..}
freezeDyn :: (Reflex t, MonadHold t m) => Dynamic t Bool -> Dynamic t a -> m (Dynamic t a)
freezeDyn predicate dyn = do
initial <- sample (current dyn)
holdDyn initial (gateDyn predicate (updated dyn))
reactiveDebugControls ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
ReactiveProgram t m (ControlState t)
reactiveDebugControls hoverAlpha = do
fastForwardClick <- fastForwardButton hoverAlpha (-4, -9)
rec speedDragged <- speedSlider hoverAlpha (-6, -9) speedFactor
playPauseClick <- playPauseButton hoverAlpha running (-8, -9)
speedFactor <-
foldDyn ($) 1 $
mergeWith
(.)
[ (\s -> if s == 0 then 1 else 0) <$ playPauseClick,
(\s -> max 2.0 (s + 1)) <$ fastForwardClick,
const <$> speedDragged
]
let running = (> 0) <$> speedFactor
rec resetViewClick <- resetViewButton hoverAlpha (9, -3) needsReset
zoomFactor <- zoomControls hoverAlpha (9, -6) resetViewClick
panOffset <- panControls running resetViewClick
let needsReset =
(||) <$> ((/= 1) <$> zoomFactor)
<*> ((/= (0, 0)) <$> panOffset)
stepClick <- stepButton hoverAlpha (-2, -9) running
transformUserPicture $ uncurry translated <$> panOffset
transformUserPicture $ dilated <$> zoomFactor
return $ ControlState
{ csRunning = running,
csTimeDilation = speedFactor,
csPointTransform = transformPoint <$> zoomFactor <*> panOffset,
csSyntheticStep = stepClick
}
where
transformPoint z (dx, dy) (x, y) = ((x - dx) / z, (y - dy) / z)
{-# WARNING
debugReactiveOf
[ "After the current migration is complete,",
"debugReactiveOf will probably be renamed to debugReflexOf."
]
#-}
getHoverAlpha :: ReflexCodeWorld t m => m (Dynamic t Double)
getHoverAlpha = do
time <- getTimePassing
move <- updated <$> getPointerPosition
rec timeSinceMove <-
foldDyn ($) 999 $
mergeWith
(.)
[ (+) <$> gateDyn ((< 5) <$> timeSinceMove) time,
const 0 <$ move
]
return (alphaFromTime <$> timeSinceMove)
where
alphaFromTime t
| t < 4.5 = 1
| t > 5.0 = 0
| otherwise = 10 - 2 * t
playPauseButton ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Dynamic t Bool ->
Point ->
ReactiveProgram t m (Event t ())
playPauseButton hoverAlpha running pos = do
systemDraw $
uncurry translated pos
<$> (bool (playButton <$> hoverAlpha) (pauseButton <$> hoverAlpha) =<< running)
click <- ffilter (onRect 0.8 0.8 pos) <$> getPointerClick
return $ () <$ click
where
playButton a =
colored
(RGBA 0 0 0 a)
(solidPolygon [(-0.2, 0.25), (-0.2, -0.25), (0.2, 0)])
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
pauseButton a =
colored
(RGBA 0 0 0 a)
( translated (-0.15) 0 (solidRectangle 0.2 0.6)
<> translated 0.15 0 (solidRectangle 0.2 0.6)
)
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
stepButton ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
Dynamic t Bool ->
ReactiveProgram t m (Event t ())
stepButton hoverAlpha pos running = do
systemDraw $
uncurry translated pos
<$> (bool (button <$> hoverAlpha) (constDyn blank) =<< running)
click <- gateDyn (not <$> running) <$> ffilter (onRect 0.8 0.8 pos) <$> getPointerClick
return $ () <$ click
where
button a =
colored
(RGBA 0 0 0 a)
( translated (-0.15) 0 (solidRectangle 0.2 0.5)
<> solidPolygon [(0.05, 0.25), (0.05, -0.25), (0.3, 0)]
)
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
fastForwardButton ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
ReactiveProgram t m (Event t ())
fastForwardButton hoverAlpha pos = do
systemDraw $ uncurry translated pos <$> button <$> hoverAlpha
click <- ffilter (onRect 0.8 0.8 pos) <$> getPointerClick
return $ () <$ click
where
button a =
colored
(RGBA 0 0 0 a)
( solidPolygon [(-0.3, 0.25), (-0.3, -0.25), (-0.05, 0)]
<> solidPolygon [(0.05, 0.25), (0.05, -0.25), (0.3, 0)]
)
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
speedSlider ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
Dynamic t Double ->
ReactiveProgram t m (Event t Double)
speedSlider hoverAlpha pos speedFactor = do
systemDraw $ uncurry translated pos <$> (slider <$> hoverAlpha <*> speedFactor)
click <- ffilter (onRect 3.0 0.8 pos) <$> getPointerClick
release <- ffilter not <$> updated <$> isPointerDown
dragging <- holdDyn False $ mergeWith (&&) [True <$ click, False <$ release]
pointer <- getPointerPosition
return $ speedFromPoint <$> mergeWith const [gateDyn dragging (updated pointer), click]
where
speedFromPoint (x, _y) = scaleRange (-1.4, 1.4) (0, 5) (x - fst pos)
xFromSpeed speed = scaleRange (0, 5) (-1.4, 1.4) speed
slider a speed =
let xoff = xFromSpeed speed
in colored
(RGBA 0 0 0 a)
( translated xoff 0.75 $ scaled 0.5 0.5 $
lettering (T.pack (showFFloatAlt (Just 2) speed "x"))
)
<> colored (RGBA 0 0 0 a) (translated xoff 0 (solidRectangle 0.2 0.8))
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 2.8 0.25)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 2.8 0.25)
resetViewButton ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
Dynamic t Bool ->
ReactiveProgram t m (Event t ())
resetViewButton hoverAlpha pos needsReset = do
click <- gateDyn needsReset . ffilter (onRect 0.8 0.8 pos) <$> getPointerClick
systemDraw $ uncurry translated pos <$> (bool (constDyn blank) (button <$> hoverAlpha) =<< needsReset)
return $ () <$ click
where
button a =
colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.7 0.2)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.2 0.7)
<> colored (RGBA 0.0 0.0 0.0 a) (thickRectangle 0.1 0.5 0.5)
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
panControls ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Bool ->
Event t () ->
ReactiveProgram t m (Dynamic t (Double, Double))
panControls running resetClick = do
click <- gateDyn (not <$> running) <$> getPointerClick
release <- ffilter not <$> updated <$> isPointerDown
dragging <- holdDyn False $ mergeWith (&&) [True <$ click, False <$ release]
pos <- getPointerPosition
let dragPos = bool (const Nothing) Just <$> dragging <*> pos
diffPairs <- foldDyn (\x (y, _) -> (x, y)) (Nothing, Nothing) (updated dragPos)
let drags = fmapMaybe toMovement (updated diffPairs)
foldDyn ($) (0, 0) $
mergeWith
(.)
[ vectorSum <$> drags,
const (0, 0) <$ resetClick
]
where
toMovement (Just (x1, y1), Just (x2, y2)) = Just (x1 - x2, y1 - y2)
toMovement _ = Nothing
zoomControls ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
Event t () ->
ReactiveProgram t m (Dynamic t Double)
zoomControls hoverAlpha (x, y) resetClick = do
zoomInClick <- zoomInButton hoverAlpha (x, y + 2)
zoomOutClick <- zoomOutButton hoverAlpha (x, y - 2)
rec zoomDrag <- zoomSlider hoverAlpha (x, y) zoomFactor
zoomFactor <-
foldDyn ($) 1 $
mergeWith
(.)
[ (* zoomIncrement) <$ zoomInClick,
(/ zoomIncrement) <$ zoomOutClick,
const <$> zoomDrag,
const 1 <$ resetClick
]
return zoomFactor
zoomInButton ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
ReactiveProgram t m (Event t ())
zoomInButton hoverAlpha pos = do
systemDraw $ uncurry translated pos <$> button <$> hoverAlpha
(() <$) <$> ffilter (onRect 0.8 0.8 pos) <$> getPointerClick
where
button a =
colored
(RGBA 0 0 0 a)
( translated
(-0.05)
(0.05)
( thickCircle 0.1 0.22
<> solidRectangle 0.06 0.25
<> solidRectangle 0.25 0.06
<> rotated (- pi / 4) (translated 0.35 0 (solidRectangle 0.2 0.1))
)
)
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
zoomOutButton ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
ReactiveProgram t m (Event t ())
zoomOutButton hoverAlpha pos = do
systemDraw $ uncurry translated pos <$> button <$> hoverAlpha
(() <$) <$> ffilter (onRect 0.8 0.8 pos) <$> getPointerClick
where
button a =
colored
(RGBA 0 0 0 a)
( translated
(-0.05)
(0.05)
( thickCircle 0.1 0.22
<> solidRectangle 0.25 0.06
<> rotated (- pi / 4) (translated 0.35 0 (solidRectangle 0.2 0.1))
)
)
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.8 0.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.8 0.8)
zoomSlider ::
( PerformEvent t m,
Adjustable t m,
MonadIO (Performable m),
PostBuild t m,
MonadHold t m,
MonadFix m
) =>
Dynamic t Double ->
Point ->
Dynamic t Double ->
ReactiveProgram t m (Event t Double)
zoomSlider hoverAlpha pos factor = do
systemDraw $ uncurry translated pos <$> (slider <$> hoverAlpha <*> factor)
click <- ffilter (onRect 0.8 3.0 pos) <$> getPointerClick
release <- ffilter not <$> updated <$> isPointerDown
dragging <- holdDyn False $ mergeWith (&&) [True <$ click, False <$ release]
pointer <- getPointerPosition
return $ zoomFromPoint <$> mergeWith const [gateDyn dragging (updated pointer), click]
where
zoomFromPoint (_x, y) = zoomIncrement ** (scaleRange (-1.4, 1.4) (-10, 10) (y - snd pos))
yFromZoom z = scaleRange (-10, 10) (-1.4, 1.4) (logBase zoomIncrement z)
slider a z =
let yoff = yFromZoom z
in colored
(RGBA 0 0 0 a)
( translated (-1.1) yoff $ scaled 0.5 0.5 $
lettering (T.pack (show (round (z * 100) :: Int) ++ "%"))
)
<> colored (RGBA 0 0 0 a) (translated 0 yoff (solidRectangle 0.8 0.2))
<> colored (RGBA 0.2 0.2 0.2 a) (rectangle 0.25 2.8)
<> colored (RGBA 0.8 0.8 0.8 a) (solidRectangle 0.25 2.8)
zoomIncrement :: Double
zoomIncrement = 8 ** (1 / 10)
onRect :: Double -> Double -> Point -> Point -> Bool
onRect w h (x1, y1) (x2, y2) = abs (x1 - x2) < w / 2 && abs (y1 - y2) < h / 2
scaleRange :: (Double, Double) -> (Double, Double) -> Double -> Double
scaleRange (a1, b1) (a2, b2) x = min b2 $ max a2 $ (x - a1) / (b1 - a1) * (b2 - a2) + a2