diff --git a/README.md b/README.md index b58064d..c8a5d7f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # purescript-behaviors - [Example](test/Main.purs) -- [Moving Gears Example](https://github.com/knaman2609/purescript-behaviors-example) - [Demo](https://github.com/paf31/purescript-behaviors-demo) - [API Documentation](generated-docs/FRP) - [Try `purescript-behaviors` in the browser](http://try.purescript.org/?backend=behaviors) diff --git a/bower.json b/bower.json index 90c665f..875e1c4 100644 --- a/bower.json +++ b/bower.json @@ -26,11 +26,15 @@ "purescript-effect": "^2.0.0", "purescript-ordered-collections": "^1.0.0", "purescript-filterable": "^3.0.0", - "purescript-nullable": "^4.0.0" + "purescript-nullable": "^4.0.0", + "purescript-event": "^1.2.4", + "purescript-web-html": "^1.0.0", + "purescript-web-events": "^1.0.0", + "purescript-web-uievents": "^1.0.0" }, "devDependencies": { "purescript-math": "^2.1.1", "purescript-integers": "^4.0.0", - "purescript-drawing": "justinwoo/purescript-drawing#compiler/0.12" + "purescript-drawing": "^4.0.0" } } diff --git a/src/FRP.purs b/src/FRP.purs deleted file mode 100644 index 1564588..0000000 --- a/src/FRP.purs +++ /dev/null @@ -1 +0,0 @@ -module FRP where diff --git a/src/FRP/Behavior.purs b/src/FRP/Behavior.purs index 09ed936..e83a9d2 100644 --- a/src/FRP/Behavior.purs +++ b/src/FRP/Behavior.purs @@ -32,7 +32,7 @@ import Data.Function (applyFlipped) import Data.Maybe (Maybe(..)) import Data.Tuple (Tuple(Tuple)) import FRP.Event (class IsEvent, Event, fix, fold, keepLatest, sampleOn, subscribe, withLast) -import FRP.Event.Time (animationFrame) +import FRP.Event.AnimationFrame (animationFrame) -- | `ABehavior` is the more general type of `Behavior`, which is parameterized -- | over some underlying `event` type. diff --git a/src/FRP/Behavior/Keyboard.purs b/src/FRP/Behavior/Keyboard.purs index 560a8b1..163a3e1 100644 --- a/src/FRP/Behavior/Keyboard.purs +++ b/src/FRP/Behavior/Keyboard.purs @@ -7,12 +7,12 @@ import Prelude import Data.Set as Set import FRP.Behavior (Behavior, behavior) -import FRP.Event.Keyboard (withKeys) +import FRP.Event.Keyboard (Keyboard, withKeys) -- | A `Behavior` which reports the keys which are currently pressed. -keys :: Behavior (Set.Set Int) -keys = behavior \e -> map (\{ value, keys: ks } -> value (Set.fromFoldable ks)) (withKeys e) +keys :: Keyboard -> Behavior (Set.Set String) +keys keyboard = behavior \e -> map (\{ value, keys: ks } -> value (Set.fromFoldable ks)) (withKeys keyboard e) -- | A `Behavior` which reports whether a specific key is currently pressed. -key :: Int -> Behavior Boolean -key k = Set.member k <$> keys +key :: Keyboard -> String -> Behavior Boolean +key keyboard k = Set.member k <$> keys keyboard diff --git a/src/FRP/Behavior/Mouse.purs b/src/FRP/Behavior/Mouse.purs index bbe2d72..9d94211 100644 --- a/src/FRP/Behavior/Mouse.purs +++ b/src/FRP/Behavior/Mouse.purs @@ -6,15 +6,14 @@ module FRP.Behavior.Mouse import Prelude import Data.Maybe (Maybe) -import Data.Nullable (toMaybe) import Data.Set as Set import FRP.Behavior (Behavior, behavior) -import FRP.Event.Mouse (withPosition, withButtons) +import FRP.Event.Mouse (Mouse, withPosition, withButtons) -- | A `Behavior` which reports the current mouse position, if it is known. -position :: Behavior (Maybe { x :: Int, y :: Int }) -position = behavior \e -> map (\{ value, pos } -> value (toMaybe pos)) (withPosition e) +position :: Mouse -> Behavior (Maybe { x :: Int, y :: Int }) +position m = behavior \e -> map (\{ value, pos } -> value pos) (withPosition m e) -- | A `Behavior` which reports the mouse buttons which are currently pressed. -buttons :: Behavior (Set.Set Int) -buttons = behavior \e -> map (\{ value, buttons: bs } -> value (Set.fromFoldable bs)) (withButtons e) +buttons :: Mouse -> Behavior (Set.Set Int) +buttons m = behavior \e -> map (\{ value, buttons: bs } -> value bs) (withButtons m e) diff --git a/src/FRP/Behavior/Time.purs b/src/FRP/Behavior/Time.purs index f22fe85..fba5c1a 100644 --- a/src/FRP/Behavior/Time.purs +++ b/src/FRP/Behavior/Time.purs @@ -1,17 +1,19 @@ module FRP.Behavior.Time - ( millisSinceEpoch + ( instant , seconds ) where import Prelude +import Data.DateTime.Instant (Instant, unInstant) +import Data.Time.Duration (Seconds, toDuration) import FRP.Behavior (Behavior, behavior) import FRP.Event.Time (withTime) -- | Get the current time in milliseconds since the epoch. -millisSinceEpoch :: Behavior Number -millisSinceEpoch = behavior \e -> map (\{ value, time: t } -> value t) (withTime e) +instant :: Behavior Instant +instant = behavior \e -> map (\{ value, time: t } -> value t) (withTime e) -- | Get the current time in seconds since the epoch. -seconds :: Behavior Number -seconds = map (_ / 1000.0) millisSinceEpoch +seconds :: Behavior Seconds +seconds = map (toDuration <<< unInstant) instant diff --git a/src/FRP/Event.js b/src/FRP/Event.js deleted file mode 100644 index 7809ddf..0000000 --- a/src/FRP/Event.js +++ /dev/null @@ -1,185 +0,0 @@ -"use strict"; - -exports.pureImpl = function (a) { - return function(sub) { - sub(a); - return function() {}; - } -}; - -exports.mapImpl = function (f) { - return function(e) { - return function (sub) { - return e(function(a) { - sub(f(a)); - }); - } - }; -}; - -exports.never = function (sub) { - return function() {}; -}; - -exports.applyImpl = function (e1) { - return function (e2) { - return function(sub) { - var a_latest, b_latest; - var a_fired = false, b_fired = false; - - var cancel1 = e1(function(a) { - a_latest = a; - a_fired = true; - - if (b_fired) { - sub(a_latest(b_latest)); - } - }); - - var cancel2 = e2(function(b) { - b_latest = b; - b_fired = true; - - if (a_fired) { - sub(a_latest(b_latest)); - } - }); - - return function() { - cancel1(); - cancel2(); - }; - }; - }; -}; - -exports.mergeImpl = function (e1) { - return function(e2) { - return function(sub) { - var cancel1 = e1(sub); - var cancel2 = e2(sub); - - return function() { - cancel1(); - cancel2(); - }; - } - }; -}; - -exports.fold = function (f) { - return function(e) { - return function(b) { - return function(sub) { - var result = b; - - return e(function(a) { - sub(result = f(a)(result)); - }); - }; - }; - }; -}; - -exports.filter = function (p) { - return function(e) { - return function(sub) { - return e(function(a) { - if (p(a)) { - sub(a); - } - }); - }; - }; -}; - -exports.sampleOn = function (e1) { - return function (e2) { - return function(sub) { - var latest; - var fired = false; - - var cancel1 = e1(function(a) { - latest = a; - fired = true; - }); - - var cancel2 = e2(function(f) { - if (fired) { - sub(f(latest)); - } - }); - - return function() { - cancel1(); - cancel2(); - }; - }; - }; -}; - -exports.subscribe = function (e) { - return function(f) { - return function() { - return e(function(a) { - f(a)(); - }); - }; - }; -}; - -exports.keepLatest = function (e) { - return function(sub) { - var cancelInner; - - var cancelOuter = e(function(inner) { - cancelInner && cancelInner(); - cancelInner = inner(sub); - }); - - return function() { - cancelInner && cancelInner(); - cancelOuter(); - } - }; -}; - -exports.create = function () { - var subs = []; - return { - event: function(sub) { - subs.push(sub); - return function() { - var index = subs.indexOf(sub); - if (index >= 0) { - subs.splice(index, 1); - } - }; - }, - push: function(a) { - return function() { - for (var i = 0; i < subs.length; i++) { - subs[i](a); - } - }; - } - }; -}; - -exports.fix = function(f) { - var s = exports.create(); - var io = f(s.event); - - return function(sub) { - var sub1 = function(a) { - s.push(a)(); - }; - var cancel1 = io.input(sub1); - var cancel2 = io.output(sub); - - return function() { - cancel1(); - cancel2(); - }; - }; -}; diff --git a/src/FRP/Event.purs b/src/FRP/Event.purs deleted file mode 100644 index 772a2d6..0000000 --- a/src/FRP/Event.purs +++ /dev/null @@ -1,124 +0,0 @@ -module FRP.Event - ( Event - , create - , subscribe - , module Class - ) where - -import Prelude - -import Control.Alternative (class Alt, class Alternative, class Plus) -import Control.Apply (lift2) -import Data.Compactable (class Compactable) -import Data.Either (either, hush) -import Data.Filterable (class Filterable, filterMap) -import Data.Maybe (Maybe(..), fromJust, isJust) -import Effect (Effect) -import FRP.Event.Class (class Filterable, class IsEvent, cleared, count, filterMap, fix, fold, folded, gate, gateBy, keepLatest, mapAccum, sampleOn, sampleOn_, withLast) as Class -import Partial.Unsafe (unsafePartial) - --- | An `Event` represents a collection of discrete occurrences with associated --- | times. Conceptually, an `Event` is a (possibly-infinite) list of values-and-times: --- | --- | ```purescript --- | type Event a = List { value :: a, time :: Time } --- | ``` --- | --- | Events are created from real events like timers or mouse clicks, and then --- | combined using the various functions and instances provided in this module. --- | --- | Events are consumed by providing a callback using the `subscribe` function. -data Event a - -foreign import pureImpl :: forall a. a -> Event a - -foreign import mapImpl :: forall a b. (a -> b) -> Event a -> Event b - -foreign import mergeImpl :: forall a. Event a -> Event a -> Event a - -foreign import never :: forall a. Event a - -instance functorEvent :: Functor Event where - map = mapImpl - -instance compactableEvent :: Compactable Event where - compact e = filterMap identity e - separate e = - { left: filterMap (either Just (const Nothing)) e - , right: filterMap (either (const Nothing) Just) e - } - -instance filterableEvent :: Filterable Event where - filter = filter - - filterMap f = unsafePartial (map fromJust <<< filter isJust <<< map f) - - partition p xs = { yes: filter p xs, no: filter (not <<< p) xs } - - partitionMap f xs = - { left: filterMap (either Just (const Nothing) <<< f) xs - , right: filterMap (hush <<< f) xs - } - -instance applyEvent :: Apply Event where - apply = applyImpl - -instance applicativeEvent :: Applicative Event where - pure = pureImpl - -instance altEvent :: Alt Event where - alt = mergeImpl - -instance plusEvent :: Plus Event where - empty = never - -instance alternativeEvent :: Alternative Event - -instance semigroupEvent :: Semigroup a => Semigroup (Event a) where - append = lift2 append - -instance monoidEvent :: Monoid a => Monoid (Event a) where - mempty = pure mempty - -instance eventIsEvent :: Class.IsEvent Event where - fold = fold - keepLatest = keepLatest - sampleOn = sampleOn - fix = fix - --- | Create an `Event` which combines with the latest values from two other events. -foreign import applyImpl :: forall a b. Event (a -> b) -> Event a -> Event b - --- | Fold over values received from some `Event`, creating a new `Event`. -foreign import fold :: forall a b. (a -> b -> b) -> Event a -> b -> Event b - --- | Create an `Event` which only fires when a predicate holds. -foreign import filter :: forall a. (a -> Boolean) -> Event a -> Event a - --- | Create an `Event` which samples the latest values from the first event --- | at the times when the second event fires. -foreign import sampleOn :: forall a b. Event a -> Event (a -> b) -> Event b - --- | Flatten a nested `Event`, reporting values only from the most recent --- | inner `Event`. -foreign import keepLatest :: forall a. Event (Event a) -> Event a - --- | Compute a fixed point -foreign import fix :: forall i o. (Event i -> { input :: Event i, output :: Event o }) -> Event o - --- | Subscribe to an `Event` by providing a callback. --- | --- | `subscribe` returns a canceller function. -foreign import subscribe - :: forall a r - . Event a - -> (a -> Effect r) - -> Effect (Effect Unit) - --- | Create an event and a function which supplies a value to that event. -foreign import create - :: forall a - . Effect - { event :: Event a - , push :: a -> Effect Unit - } diff --git a/src/FRP/Event/AnimationFrame.purs b/src/FRP/Event/AnimationFrame.purs new file mode 100644 index 0000000..a1bbd59 --- /dev/null +++ b/src/FRP/Event/AnimationFrame.purs @@ -0,0 +1,21 @@ +module FRP.Event.AnimationFrame + ( animationFrame + ) where + +import Prelude +import Effect.Ref as Ref +import FRP.Event (Event, makeEvent) +import Web.HTML (window) +import Web.HTML.Window (requestAnimationFrame) + +-- | Create an event which fires every frame (using `requestAnimationFrame`). +animationFrame :: Event Unit +animationFrame = makeEvent \k -> do + w <- window + cancelled <- Ref.new false + let loop = void do + w # requestAnimationFrame do + k unit + unlessM (Ref.read cancelled) loop + loop + pure (Ref.write true cancelled) \ No newline at end of file diff --git a/src/FRP/Event/Class.purs b/src/FRP/Event/Class.purs deleted file mode 100644 index 2bfc4e0..0000000 --- a/src/FRP/Event/Class.purs +++ /dev/null @@ -1,91 +0,0 @@ -module FRP.Event.Class - ( class IsEvent - , fold - , folded - , count - , mapAccum - , withLast - , sampleOn - , sampleOn_ - , keepLatest - , fix - , gate - , gateBy - , module Data.Filterable - ) where - -import Prelude - -import Control.Alternative (class Alternative, (<|>)) -import Data.Filterable (class Filterable, cleared, filterMap) -import Data.Maybe (Maybe(..), fromMaybe) -import Data.Tuple (Tuple(..), snd) - --- | Functions which an `Event` type should implement, so that --- | `Behavior`s can be defined in terms of any such event type: --- | --- | - `fold`: combines incoming values using the specified function, --- | starting with the specific initial value. --- | - `keepLatest` flattens a nested event, reporting values only from the --- | most recent inner event. --- | - `sampleOn`: samples an event at the times when a second event fires. --- | - `fix`: compute a fixed point, by feeding output events back in as --- | inputs. -class (Alternative event, Filterable event) <= IsEvent event where - fold :: forall a b. (a -> b -> b) -> event a -> b -> event b - keepLatest :: forall a. event (event a) -> event a - sampleOn :: forall a b. event a -> event (a -> b) -> event b - fix :: forall i o. (event i -> { input :: event i, output :: event o }) -> event o - --- | Count the number of events received. -count :: forall event a. IsEvent event => event a -> event Int -count s = fold (\_ n -> n + 1) s 0 - --- | Combine subsequent events using a `Monoid`. -folded :: forall event a. IsEvent event => Monoid a => event a -> event a -folded s = fold append s mempty - --- | Compute differences between successive event values. -withLast :: forall event a. IsEvent event => event a -> event { now :: a, last :: Maybe a } -withLast e = filterMap identity (fold step e Nothing) where - step a Nothing = Just { now: a, last: Nothing } - step a (Just { now: b }) = Just { now: a, last: Just b } - --- | Map over an event with an accumulator. --- | --- | For example, to keep the index of the current event: --- | --- | ```purescript --- | mapAccum (\x i -> Tuple (i + 1) (Tuple x i)) 0 --- | ``` -mapAccum :: forall event a b c. IsEvent event => (a -> b -> Tuple b c) -> b -> event a -> event c -mapAccum f acc xs = filterMap snd - $ fold (\a (Tuple b _) -> pure <$> f a b) xs - $ Tuple acc Nothing - --- | Create an `Event` which samples the latest values from the first event --- | at the times when the second event fires, ignoring the values produced by --- | the second event. -sampleOn_ :: forall event a b. IsEvent event => event a -> event b -> event a -sampleOn_ a b = sampleOn a (b $> identity) - --- | Sample the events that are fired while a boolean event is true. Note that, --- | until the boolean event fires, it will be assumed to be `false`, and events --- | will be blocked. -gate :: forall a event. IsEvent event => event Boolean -> event a -> event a -gate = gateBy (\x _ -> fromMaybe false x) - --- | Generalised form of `gateBy`, allowing for any predicate between the two --- | events. Until a value from the first event is received, `Nothing` will be --- | passed to the predicate. -gateBy - :: forall a b event - . IsEvent event - => (Maybe a -> b -> Boolean) - -> event a - -> event b - -> event b -gateBy f sampled - = cleared - <<< sampleOn (pure Nothing <|> (Just <$> sampled)) - <<< map \x p -> if f p x then Just x else Nothing diff --git a/src/FRP/Event/Keyboard.js b/src/FRP/Event/Keyboard.js deleted file mode 100644 index 8167498..0000000 --- a/src/FRP/Event/Keyboard.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -var currentKeys = []; -addEventListener("keydown", function(e) { - var index = currentKeys.indexOf(e.keyCode); - if (index < 0) { - currentKeys.push(e.keyCode); - } -}); -addEventListener("keyup", function(e) { - var index = currentKeys.indexOf(e.keyCode); - if (index >= 0) { - currentKeys.splice(index, 1); - } -}); - -exports.withKeys = function (e) { - return function(sub) { - return e(function(a) { - sub({ keys: currentKeys, value: a }); - }); - }; -}; - -exports.down = function(sub) { - var cb = function(e) { - sub(e.keyCode); - }; - addEventListener("keydown", cb); - return function() { - removeEventListener("keydown", cb); - } -}; - -exports.up = function(sub) { - var cb = function(e) { - sub(e.keyCode); - }; - addEventListener("keyup", cb); - return function() { - removeEventListener("keyup", cb); - } -}; diff --git a/src/FRP/Event/Keyboard.purs b/src/FRP/Event/Keyboard.purs index 3e76ae0..4e2e8f4 100644 --- a/src/FRP/Event/Keyboard.purs +++ b/src/FRP/Event/Keyboard.purs @@ -1,16 +1,79 @@ module FRP.Event.Keyboard - ( down + ( Keyboard + , getKeyboard + , disposeKeyboard + , down , up , withKeys ) where -import FRP.Event (Event) +import Prelude + +import Data.Foldable (traverse_) +import Data.Newtype (wrap) +import Data.Set as Set +import Effect (Effect) +import Effect.Ref as Ref +import FRP.Event (Event, makeEvent, subscribe) +import Web.Event.EventTarget (addEventListener, eventListener, removeEventListener) +import Web.HTML (window) +import Web.HTML.Window (toEventTarget) +import Web.UIEvent.KeyboardEvent (code, fromEvent) + +-- | A handle for creating events from the keyboard. +newtype Keyboard = Keyboard + { keys :: Ref.Ref (Set.Set String) + , dispose :: Effect Unit + } + +-- | Get a handle for working with the keyboard. +getKeyboard :: Effect Keyboard +getKeyboard = do + keys <- Ref.new Set.empty + target <- toEventTarget <$> window + keyDownListener <- eventListener \e -> do + fromEvent e # traverse_ \ke -> + Ref.modify (Set.insert (code ke)) keys + keyUpListener <- eventListener \e -> do + fromEvent e # traverse_ \ke -> + Ref.modify (Set.delete (code ke)) keys + addEventListener (wrap "keydown") keyDownListener false target + addEventListener (wrap "keyup") keyUpListener false target + let dispose = do + removeEventListener (wrap "keydown") keyDownListener false target + removeEventListener (wrap "keyup") keyUpListener false target + pure (Keyboard { keys, dispose }) + +disposeKeyboard :: Keyboard -> Effect Unit +disposeKeyboard (Keyboard { dispose }) = dispose -- | Create an `Event` which fires when a key is pressed -foreign import down :: Event Int +down :: Event String +down = makeEvent \k -> do + target <- toEventTarget <$> window + keyDownListener <- eventListener \e -> do + fromEvent e # traverse_ \ke -> + k (code ke) + addEventListener (wrap "keydown") keyDownListener false target + pure (removeEventListener (wrap "keydown") keyDownListener false target) -- | Create an `Event` which fires when a key is released -foreign import up :: Event Int +up :: Event String +up = makeEvent \k -> do + target <- toEventTarget <$> window + keyUpListener <- eventListener \e -> do + fromEvent e # traverse_ \ke -> + k (code ke) + addEventListener (wrap "keyup") keyUpListener false target + pure (removeEventListener (wrap "keyup") keyUpListener false target) --- | Create an event which also returns the current pressed keycodes. -foreign import withKeys :: forall a. Event a -> Event { value :: a, keys :: Array Int } +-- | Create an event which also returns the currently pressed keys. +withKeys + :: forall a + . Keyboard + -> Event a + -> Event { value :: a, keys :: Set.Set String } +withKeys (Keyboard { keys }) e = makeEvent \k -> + e `subscribe` \value -> do + keysValue <- Ref.read keys + k { value, keys: keysValue } diff --git a/src/FRP/Event/Mouse.js b/src/FRP/Event/Mouse.js deleted file mode 100644 index 58fa3e1..0000000 --- a/src/FRP/Event/Mouse.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; - -var currentPosition; -addEventListener("mousemove", function(e) { - currentPosition = { x: e.clientX, y: e.clientY }; -}); - -var currentButtons = []; -addEventListener("mousedown", function(e) { - currentButtons.push(e.button); -}); -addEventListener("mouseup", function(e) { - var index = currentButtons.indexOf(e.button); - if (index >= 0) { - currentButtons.splice(index, 1); - } -}); - -exports.withPosition = function (e) { - return function(sub) { - return e(function(a) { - sub({ pos: currentPosition, value: a }); - }); - }; -}; - -exports.withButtons = function (e) { - return function(sub) { - return e(function(a) { - sub({ buttons: currentButtons, value: a }); - }); - }; -}; - -exports.move = function(sub) { - var cb = function(e) { - sub({ x: e.clientX, y: e.clientY }); - }; - addEventListener("mousemove", cb); - return function() { - removeEventListener("mousemove", cb); - }; -}; - -exports.down = function(sub) { - var cb = function(e) { - sub(e.button); - }; - addEventListener("mousedown", cb); - return function() { - removeEventListener("mousedown", cb); - }; -}; - -exports.up = function(sub) { - var cb = function(e) { - sub(e.button); - }; - addEventListener("mouseup", cb); - return function() { - removeEventListener("mouseup", cb); - }; -}; diff --git a/src/FRP/Event/Mouse.purs b/src/FRP/Event/Mouse.purs index ce3f3a3..824fa5b 100644 --- a/src/FRP/Event/Mouse.purs +++ b/src/FRP/Event/Mouse.purs @@ -1,25 +1,105 @@ module FRP.Event.Mouse - ( move + ( Mouse + , getMouse + , disposeMouse + , move , down , up , withPosition , withButtons ) where -import Data.Nullable (Nullable) -import FRP.Event (Event) +import Prelude --- | Create an `Event` which fires when the mouse moves -foreign import move :: Event { x :: Int, y :: Int } +import Data.Compactable (compact) +import Data.Foldable (traverse_) +import Data.Maybe (Maybe(..)) +import Data.Newtype (wrap) +import Data.Set as Set +import Effect (Effect) +import Effect.Ref as Ref +import FRP.Event (Event, makeEvent, subscribe) +import Web.Event.EventTarget (addEventListener, eventListener, removeEventListener) +import Web.HTML (window) +import Web.HTML.Window (toEventTarget) +import Web.UIEvent.MouseEvent (button, clientX, clientY, fromEvent) --- | Create an event which also returns the current mouse position. -foreign import withPosition :: forall a. Event a -> Event { value :: a, pos :: Nullable { x :: Int, y :: Int } } +-- | A handle for creating events from the mouse position and buttons. +newtype Mouse = Mouse + { position :: Ref.Ref (Maybe { x :: Int, y :: Int }) + , buttons :: Ref.Ref (Set.Set Int) + , dispose :: Effect Unit + } + +-- | Get a handle for working with the mouse. +getMouse :: Effect Mouse +getMouse = do + position <- Ref.new Nothing + buttons <- Ref.new Set.empty + target <- toEventTarget <$> window + mouseMoveListener <- eventListener \e -> do + fromEvent e # traverse_ \me -> + Ref.write (Just { x: clientX me, y: clientY me }) position + mouseDownListener <- eventListener \e -> do + fromEvent e # traverse_ \me -> + Ref.modify (Set.insert (button me)) buttons + mouseUpListener <- eventListener \e -> do + fromEvent e # traverse_ \me -> + Ref.modify (Set.delete (button me)) buttons + addEventListener (wrap "mousemove") mouseMoveListener false target + addEventListener (wrap "mousedown") mouseDownListener false target + addEventListener (wrap "mouseup") mouseUpListener false target + let dispose = do + removeEventListener (wrap "mousemove") mouseMoveListener false target + removeEventListener (wrap "mousedown") mouseDownListener false target + removeEventListener (wrap "mouseup") mouseUpListener false target + pure (Mouse { position, buttons, dispose }) + +disposeMouse :: Mouse -> Effect Unit +disposeMouse (Mouse { dispose }) = dispose + +-- | Create an `Event` which fires when the mouse moves +move :: Mouse -> Event { x :: Int, y :: Int } +move m = compact (_.pos <$> withPosition m (pure unit)) -- | Create an `Event` which fires when a mouse button is pressed -foreign import down :: Event Int +down :: Event Int +down = makeEvent \k -> do + target <- toEventTarget <$> window + mouseDownListener <- eventListener \e -> do + fromEvent e # traverse_ \me -> + k (button me) + addEventListener (wrap "mousedown") mouseDownListener false target + pure (removeEventListener (wrap "mousedown") mouseDownListener false target) -- | Create an `Event` which fires when a mouse button is released -foreign import up :: Event Int +up :: Event Int +up = makeEvent \k -> do + target <- toEventTarget <$> window + mouseUpListener <- eventListener \e -> do + fromEvent e # traverse_ \me -> + k (button me) + addEventListener (wrap "mouseup") mouseUpListener false target + pure (removeEventListener (wrap "mouseup") mouseUpListener false target) + +-- | Create an event which also returns the current mouse position. +withPosition + :: forall a + . Mouse + -> Event a + -> Event { value :: a, pos :: Maybe { x :: Int, y :: Int } } +withPosition (Mouse { position }) e = makeEvent \k -> + e `subscribe` \value -> do + pos <- Ref.read position + k { value, pos } -- | Create an event which also returns the current mouse buttons. -foreign import withButtons :: forall a. Event a -> Event { value :: a, buttons :: Array Int } +withButtons + :: forall a + . Mouse + -> Event a + -> Event { value :: a, buttons :: Set.Set Int } +withButtons (Mouse { buttons }) e = makeEvent \k -> + e `subscribe` \value -> do + buttonsValue <- Ref.read buttons + k { value, buttons: buttonsValue } diff --git a/src/FRP/Event/Semantic.purs b/src/FRP/Event/Semantic.purs index 6b61b10..856b2e3 100644 --- a/src/FRP/Event/Semantic.purs +++ b/src/FRP/Event/Semantic.purs @@ -31,7 +31,7 @@ -- | The meaning of the sampling function `b` is then the function -- | -- | ```purescript --- | \t -> valueOf (sample b (once t identity)) +-- | \t -> valueOf (sample b (once t identityentity)) -- | ``` -- | -- | where @@ -106,11 +106,11 @@ import Control.Alt (class Alt) import Control.Alternative (class Alternative, class Plus) import Control.Apply (lift2) import Data.Compactable (class Compactable) -import Data.Either (Either(..), either) +import Data.Either (Either(..)) import Data.Filterable (class Filterable, filter, filterMap, partition, partitionMap) import Data.List (List(..), (:)) import Data.List as List -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe) import Data.Newtype (class Newtype) import Data.Traversable (mapAccumL, traverse) import Data.Tuple (Tuple(..), fst, snd) @@ -178,11 +178,8 @@ instance monoidSemantic :: (Bounded time, Monoid a) => Monoid (Semantic time a) mempty = pure mempty instance compactableSemantic :: Compactable (Semantic time) where - compact e = filterMap identity e - separate e = - { left: filterMap (either Just (const Nothing)) e - , right: filterMap (either (const Nothing) Just) e - } + compact = filterMap identity + separate = partitionMap identity instance filterableSemantic :: Filterable (Semantic time) where filter p (Semantic xs) = Semantic (filter (p <<< snd) xs) diff --git a/src/FRP/Event/Time.js b/src/FRP/Event/Time.js deleted file mode 100644 index b895c38..0000000 --- a/src/FRP/Event/Time.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; - -exports.interval = function (n) { - return function(sub) { - var interval = setInterval(function() { - sub(new Date().getTime()); - }, n); - return function() { - clearInterval(interval); - }; - }; -}; - -exports.animationFrame = function(sub) { - var cancelled = false; - var loop = function() { - window.requestAnimationFrame(function() { - sub(); - if (!cancelled) { - loop(); - } - }); - }; - loop(); - return function() { - cancelled = true; - } -}; - -exports.withTime = function (e) { - return function(sub) { - return e(function(a) { - var time = new Date().getTime(); - sub({ time: time, value: a }); - }); - }; -}; diff --git a/src/FRP/Event/Time.purs b/src/FRP/Event/Time.purs deleted file mode 100644 index 55b67dc..0000000 --- a/src/FRP/Event/Time.purs +++ /dev/null @@ -1,57 +0,0 @@ -module FRP.Event.Time - ( interval - , animationFrame - , withTime - , debounce - , debounceWith - ) where - -import Data.Maybe (Maybe, maybe) -import Data.Unit (Unit) -import FRP.Event (Event) -import FRP.Event.Class (fix, gateBy) -import Prelude ((+), (<), map) - --- | Create an event which fires every specified number of milliseconds. -foreign import interval :: Int -> Event Int - --- | Create an event which fires every frame (using `requestAnimationFrame`). -foreign import animationFrame :: Event Unit - --- | Create an event which reports the current time in milliseconds since the epoch. -foreign import withTime :: forall a. Event a -> Event { value :: a, time :: Number } - --- | On each event, ignore subsequent events for a given number of milliseconds. -debounce :: forall a. Number -> Event a -> Event a -debounce period = debounceWith (map { period, value: _ }) - --- | Provided an input event and transformation, block the input event for the --- | duration of the specified period on each output. -debounceWith - :: forall a b. - (Event a -> Event { period :: Number, value :: b }) - -> Event a - -> Event b -debounceWith process event - = fix \allowed -> - let - processed :: Event { period :: Number, value :: b } - processed = process allowed - - expiries :: Event Number - expiries = - map (\{ time, value } -> time + value) - (withTime (map _.period processed)) - - comparison :: forall r. Maybe Number -> { time :: Number | r } -> Boolean - comparison a b = maybe true (_ < b.time) a - - unblocked :: Event { time :: Number, value :: a } - unblocked = gateBy comparison expiries stamped - in - { input: map _.value unblocked - , output: map _.value processed - } - where - stamped :: Event { time :: Number, value :: a } - stamped = withTime event diff --git a/test/Main.purs b/test/Main.purs index 48fc75e..208c5df 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -8,24 +8,23 @@ import Data.Array (sortBy, (..)) import Data.Foldable (foldMap) import Data.Int (toNumber) import Data.Maybe (fromJust, maybe) +import Data.Newtype (unwrap) import Data.Set (isEmpty) import Effect (Effect) -import FRP (FRP) import FRP.Behavior (Behavior, animate, fixB, integral', switcher) import FRP.Behavior.Mouse (buttons) import FRP.Behavior.Mouse as Mouse import FRP.Behavior.Time as Time -import FRP.Event.Class (fold) -import FRP.Event.Mouse (down) +import FRP.Event.Mouse (Mouse, getMouse, down) import Global (infinity) -import Graphics.Canvas (CANVAS, getCanvasElementById, getCanvasHeight, getCanvasWidth, getContext2D, setCanvasHeight, setCanvasWidth) +import Graphics.Canvas (getCanvasElementById, getCanvasHeight, getCanvasWidth, getContext2D, setCanvasHeight, setCanvasWidth) import Graphics.Drawing (Drawing, circle, fillColor, filled, lineWidth, outlineColor, outlined, rectangle, render, scale, translate) import Partial.Unsafe (unsafePartial) type Circle = { x :: Number, y :: Number, size :: Number } -scene :: { w :: Number, h :: Number } -> Behavior Drawing -scene { w, h } = pure background <> map renderCircles circles where +scene :: Mouse -> { w :: Number, h :: Number } -> Behavior Drawing +scene mouse { w, h } = pure background <> map renderCircles circles where background :: Drawing background = filled (fillColor blueGrey) (rectangle 0.0 0.0 w h) @@ -55,16 +54,16 @@ scene { w, h } = pure background <> map renderCircles circles where swell :: Behavior Number swell = fixB 2.0 \b -> - integral' 2.0 Time.seconds - let db = fixB 10.0 \db -> - integral' 10.0 Time.seconds (f <$> buttons <*> b <*> db) + integral' 2.0 (unwrap <$> Time.seconds) + let db = fixB 10.0 \db_ -> + integral' 10.0 (unwrap <$> Time.seconds) (f <$> buttons mouse <*> b <*> db_) in switcher db (down $> db) where f bs s ds | isEmpty bs = -8.0 * (s - 1.0) - ds * 2.0 | otherwise = 2.0 * (4.0 - s) circles :: Behavior (Array Circle) - circles = toCircles <$> Mouse.position <*> swell where + circles = toCircles <$> Mouse.position mouse <*> swell where toCircles m sw = sortBy (comparing (\{ x, y } -> -(dist x y m))) do i <- 0 .. 16 @@ -89,7 +88,8 @@ main = do ctx <- getContext2D canvas w <- getCanvasWidth canvas h <- getCanvasHeight canvas - _ <- setCanvasWidth w canvas - _ <- setCanvasHeight h canvas - _ <- animate (scene { w, h }) (render ctx) + _ <- setCanvasWidth canvas w + _ <- setCanvasHeight canvas h + mouse <- getMouse + _ <- animate (scene mouse { w, h }) (render ctx) pure unit