codeworld-api-0.5.0: src/CodeWorld/Parameter.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ParallelListComp #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE RankNTypes #-}
{-
Copyright 2019 The CodeWorld Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-}
module CodeWorld.Parameter
{-# WARNING "This is an experimental API. It can change at any time." #-}
( Parameter,
parametricDrawingOf,
slider,
toggle,
counter,
constant,
random,
timer,
currentHour,
currentMinute,
currentSecond,
converted,
renamed,
)
where
import CodeWorld
import CodeWorld.Driver (runInspect)
import Data.Function (on)
import Data.List (sortBy)
import Data.Text (Text, pack)
import qualified Data.Text as T
import Data.Time.Clock
import Data.Time.LocalTime
import Numeric (showFFloatAlt)
import System.IO.Unsafe (unsafePerformIO)
import System.Random (newStdGen, randomR)
-- | Bounds information for a parameter UI. The fields are the
-- left and top coordinate, then the width and height.
type Bounds = (Double, Double, Double, Double)
-- | The source for a parameter that can be adjusted in a parametric
-- drawing. Parameters can get their values from sliders, buttons,
-- counters, timers, etc.
data Parameter where
Parameter ::
Text ->
Double ->
Picture ->
Bounds ->
(Event -> Parameter) ->
Parameter
-- | A drawing that depends on parameters. The first argument is a
-- list of parameters. The second is a picture, which depends on the
-- values of those parameters. Each number used to retrieve the picture
-- is the value of the corresponding parameter in the first list.
parametricDrawingOf :: [Parameter] -> ([Double] -> Picture) -> IO ()
parametricDrawingOf initialParams mainPic =
runInspect
(zip [0 :: Int ..] (layoutParams 0 (-9.5) 9.5 initialParams), True, 5)
(const id)
change
picture
rawPicture
where
change (KeyPress " ") (params, vis, _) = (params, not vis, 2)
change (PointerPress pt) (params, vis, t) =
case (vis, pullMatch (hitTest pt . snd) params) of
(True, (Just p, ps)) -> (fmap (changeParam (PointerPress pt)) p : ps, vis, t)
_ -> (params, vis, t)
change event (params, vis, t) =
(map (fmap (changeParam event)) params, vis, changeTime event t)
picture (params, vis, t) =
showHideBanner t
& (picWhen vis $ pictures (map (showParam . snd) params))
& rawPicture (params, vis, t)
rawPicture (params, _, _) =
mainPic (map (getParam . snd) (sortBy (compare `on` fst) params))
changeParam event (Parameter _ _ _ _ handle) = handle event
showParam (Parameter _ _ pic _ _) = pic
getParam (Parameter _ val _ _ _) = val
changeTime (TimePassing dt) t = max 0 (t - dt)
changeTime _ t = t
showHideBanner t =
picWhen (t > 0)
$ translated 0 (-9)
$ dilated 0.5
$ colored (RGBA 0 0 0 t) (rectangle 18 2)
& colored
(RGBA 0 0 0 t)
(lettering "Press Space to show/hide parameters.")
& colored (RGBA 0.75 0.75 0.75 (min 0.8 t)) (solidRectangle 18 2)
-- | Wraps a list of parameters in frames to lay them out on the screen.
layoutParams :: Double -> Double -> Double -> [Parameter] -> [Parameter]
layoutParams _ _ _ [] = []
layoutParams maxw x y (p : ps)
| y > (-9.5) + h + titleHeight =
framedParam (x - left) (y - top - titleHeight) p
: layoutParams (max maxw w) x (y - h - titleHeight - gap) ps
| otherwise = layoutParams 0 (x + maxw + gap) 9.5 (p : ps)
where
Parameter _ _ _ (left, top, w, h) _ = p
gap = 0.5
-- | Finds the first element of a list that matches the predicate, if any,
-- and removes it from the list, returning it separately from the remaining
-- elements.
pullMatch :: (a -> Bool) -> [a] -> (Maybe a, [a])
pullMatch _ [] = (Nothing, [])
pullMatch p (a : as)
| p a = (Just a, as)
| otherwise = fmap (a :) (pullMatch p as)
-- | Determines if a point is inside the screen area for a given parameter.
hitTest :: Point -> Parameter -> Bool
hitTest (x, y) (Parameter _ _ _ (left, top, w, h) _) =
x > left && x < left + w && y < top && y > top - h
-- | Builds a parameter from an explicit state.
parameterOf ::
Text ->
state ->
(Event -> state -> state) ->
(state -> Double) ->
(state -> Picture) ->
(state -> Bounds) ->
Parameter
parameterOf name initial change value picture bounds =
Parameter
name
(value initial)
(picture initial)
(bounds initial)
(\e -> parameterOf name (change e initial) change value picture bounds)
-- Puts a simple parameter in a draggable widget that let the user
-- manipulate it on the screen, and displays the name and value.
-- All parameters are enclosed in one of these automatically.
framedParam :: Double -> Double -> Parameter -> Parameter
framedParam ix iy iparam =
parameterOf
name
(iparam, (ix, iy), True, Nothing)
framedHandle
(\(Parameter _ v _ _ _, _, _, _) -> v)
framedPicture
framedBounds
where
(Parameter name _ _ _ _) = iparam
-- | The state of a framedParam, which includes the original parameter,
-- its location, whether it's open (expanded) or not, and the anchor if
-- it is currently being dragged.
type FrameState = (Parameter, Point, Bool, Maybe Point)
framedHandle :: Event -> FrameState -> FrameState
framedHandle (PointerPress (px, py)) (param, (x, y), open, anchor)
| onOpenButton = (param, (x, y), not open, anchor)
| onTitleBar = (param, (x, y), open, Just (px, py))
where
Parameter _ _ _ (left, top, w, h) _ = param
onTitleBar =
abs (px - x - (left + w / 2)) < w / 2
&& abs (py - y - top - titleHeight / 2) < titleHeight / 2
onOpenButton
| w * h > 0 =
abs (px - x - (left + w - titleHeight / 2)) < 0.2
&& abs (py - y - (top + titleHeight / 2)) < 0.2
| otherwise = False
framedHandle (PointerRelease _) (param, loc, open, Just _) =
(param, loc, open, Nothing)
framedHandle (PointerMovement (px, py)) (param, (x, y), open, Just (ax, ay)) =
(param, (x + px - ax, y + py - ay), open, Just (px, py))
framedHandle (TimePassing dt) (Parameter _ _ _ _ handle, loc, open, anchor) =
(handle (TimePassing dt), loc, open, anchor)
framedHandle event (Parameter _ _ _ _ handle, (x, y), True, anchor) =
(handle (untranslated x y event), (x, y), True, anchor)
framedHandle _ other = other
framedPicture :: FrameState -> Picture
framedPicture (Parameter n v pic (left, top, w, h) _, (x, y), open, _) =
translated x y $
translated (left + w / 2) (top + titleHeight / 2) titleBar
& translated (left + w / 2) (top - h / 2) clientArea
where
titleBar
| w * h > 0 =
rectangle w titleHeight
& translated
((w - titleHeight) / 2)
0
(if open then collapseButton else expandButton)
& translated
(- titleHeight / 2)
0
( clipped
(w - titleHeight)
titleHeight
(dilated 0.5 (lettering titleText))
)
& colored titleColor (solidRectangle w titleHeight)
| otherwise =
rectangle w titleHeight
& clipped w titleHeight (dilated 0.5 (lettering titleText))
& colored titleColor (solidRectangle w titleHeight)
titleText
| T.length n > 10 = T.take 8 n <> "... = " <> formattedVal
| otherwise = n <> " = " <> formattedVal
formattedVal = pack (showFFloatAlt (Just 2) v "")
collapseButton = rectangle 0.4 0.4 & solidPolygon [(-0.1, -0.1), (0.1, -0.1), (0, 0.1)]
expandButton = rectangle 0.4 0.4 & solidPolygon [(-0.1, 0.1), (0.1, 0.1), (0, -0.1)]
clientArea =
picWhen (w * h > 0) $
rectangle w h
& clipped w h pic
& colored bgColor (solidRectangle 5 1)
framedBounds :: FrameState -> Bounds
framedBounds (Parameter _ _ _ (left, top, w, h) _, (x, y), True, _) =
(x + left, y + top + titleHeight, w, h + titleHeight)
framedBounds (Parameter _ _ _ (left, top, w, _) _, (x, y), False, _) =
(x + left, y + top + titleHeight, w, titleHeight)
titleHeight :: Double
titleHeight = 0.7
untranslated :: Double -> Double -> Event -> Event
untranslated x y (PointerPress (px, py)) = PointerPress (px - x, py - y)
untranslated x y (PointerRelease (px, py)) = PointerRelease (px - x, py - y)
untranslated x y (PointerMovement (px, py)) = PointerMovement (px - x, py - y)
untranslated _ _ other = other
-- | Adjusts the output of a parameter by passing it through a conversion
-- function. Built-in parameters usually range from 0 to 1, and conversions
-- can be used to rescale the output to a different range.
converted :: (Double -> Double) -> Parameter -> Parameter
converted c (Parameter name val pic bounds handle) =
Parameter name (c val) pic bounds (converted c . handle)
-- | Changes the name of an existing parameter.
renamed :: Text -> Parameter -> Parameter
renamed name (Parameter _ val pic bounds handle) =
Parameter name val pic bounds (renamed name . handle)
-- | A 'Parameter' with a constant value, and no way to change it.
constant :: Text -> Double -> Parameter
constant name n =
parameterOf
name
n
(const id)
id
(const blank)
(const (-2.5, 0, 5, 0))
-- | Builder for 'Parameter' types that are clickable and 5x1 in size.
buttonOf ::
Text ->
state ->
(state -> state) ->
(state -> Double) ->
(state -> Picture) ->
Parameter
buttonOf name initial click value pic =
parameterOf
name
(initial, False)
change
(value . fst)
( \(state, press) ->
pic state
& picWhen press (colored (RGBA 0 0 0 0.3) (solidRectangle 5 1))
)
(const (-2.5, 0.5, 5, 1))
where
change (PointerPress (px, py)) (state, _)
| abs px < 2.5, abs py < 0.5 = (state, True)
change (PointerRelease (px, py)) (state, True)
| abs px < 2.5, abs py < 0.5 = (click state, False)
| otherwise = (state, False)
change _ (state, press) = (state, press)
-- | A 'Parameter' that can be toggled between 0 (off) and 1 (on).
toggle :: Text -> Parameter
toggle name = buttonOf name False not value picture
where
value True = 1
value False = 0
picture True = dilated 0.5 $ lettering "\x2611"
picture False = dilated 0.5 $ lettering "\x2610"
-- | A 'Parameter' that counts how many times it has been clicked.
counter :: Text -> Parameter
counter name = buttonOf name 0 (+ 1) id picture
where
picture _ = dilated 0.5 (lettering "Next")
-- | A 'Parameter' that can be adjusted continuously between 0 and 1.
slider :: Text -> Parameter
slider name =
parameterOf
name
(0.5, False)
change
fst
picture
(const (-2.5, 0.5, 5, 1))
where
change (PointerPress (px, py)) (_, _)
| abs px < 2, abs py < 0.25 = (min 1 $ max 0 $ (px + 2) / 4, True)
change (PointerRelease _) (v, _) = (v, False)
change (PointerMovement (px, _)) (_, True) =
(min 1 $ max 0 $ (px + 2) / 4, True)
change _ state = state
picture (v, _) =
translated (v * 4 - 2) 0 (solidRectangle 0.125 0.5)
& solidRectangle 4 0.1
-- | A 'Parameter' that has a randomly chosen value. It offers a button to
-- regenerate its value.
random :: Text -> Parameter
random name = buttonOf name initial (next . snd) fst picture
where
initial = next (unsafePerformIO newStdGen)
picture _ = dilated 0.5 $ lettering "\x21ba Regenerate"
next = randomR (0.0, 1.0)
-- | A 'Parameter' that changes over time. It can be paused or reset.
timer :: Text -> Parameter
timer name =
parameterOf
name
(0, 1)
change
fst
picture
(const (-2.5, 0.5, 5, 1))
where
change (TimePassing dt) (t, r) = (t + r * dt, r)
change (PointerPress (px, py)) (t, r)
| abs (px - 5 / 6) < 5 / 6, abs py < 0.75 = (t, 1 - r)
| abs (px + 5 / 6) < 5 / 6, abs py < 0.75 = (0, 0)
change _ state = state
picture (_, 0) =
(translated (5 / 6) 0 $ dilated 0.5 $ lettering "\x23e9")
& (translated (-5 / 6) 0 $ dilated 0.5 $ lettering "\x23ee")
picture _ =
(translated (5 / 6) 0 $ dilated 0.5 $ lettering "\x23f8")
& (translated (-5 / 6) 0 $ dilated 0.5 $ lettering "\x23ee")
-- | A 'Parameter' that tracks the current hour, in local time. The hour
-- is on a scale from 0 (meaning midnight) to 23 (meaning 11:00 pm).
currentHour :: Parameter
currentHour =
parameterOf
"hour"
()
(const id)
(\_ -> unsafePerformIO $ fromIntegral <$> todHour <$> getTimeOfDay)
(const blank)
(const (-2.5, 0, 5, 0))
-- | A 'Parameter' that tracks the current minute, in local time. It
-- ranges from 0 to 59.
currentMinute :: Parameter
currentMinute =
parameterOf
"minute"
()
(const id)
(\_ -> unsafePerformIO $ fromIntegral <$> todMin <$> getTimeOfDay)
(const blank)
(const (-2.5, 0, 5, 0))
-- | A 'Parameter' that tracks the current second, in local time. It
-- ranges from 0.0 up to (but not including) 60.0. This includes
-- fractions of a second. If that's not what you want, you can use
-- 'withConversion' to truncate the number.
currentSecond :: Parameter
currentSecond =
parameterOf
"second"
()
(const id)
(\_ -> unsafePerformIO $ realToFrac <$> todSec <$> getTimeOfDay)
(const blank)
(const (-2.5, 0, 5, 0))
getTimeOfDay :: IO TimeOfDay
getTimeOfDay = do
now <- getCurrentTime
timezone <- getCurrentTimeZone
return (localTimeOfDay (utcToLocalTime timezone now))
titleColor :: Color
titleColor = RGBA 0.7 0.7 0.7 0.9
bgColor :: Color
bgColor = RGBA 0.8 0.85 0.95 0.8
picWhen :: Bool -> Picture -> Picture
picWhen True = id
picWhen False = const blank