----------------------------------------------------------------------------- {-# LANGUAGE OverloadedStrings #-} ----------------------------------------------------------------------------- -- | -- Module : Miso.Subscription.RAF -- Copyright : (C) 2016-2026 David M. Johnson -- License : BSD3-style (see the file LICENSE) -- Maintainer : David M. Johnson <code@dmj.io> -- Stability : experimental -- Portability : non-portable -- -- = Overview -- -- "Miso.Subscription.RAF" provides 'rAFSub', a subscription that hooks -- into the browser's -- <https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame requestAnimationFrame> -- loop. On each frame the browser calls back with a -- <https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp DOMHighResTimeStamp> -- (milliseconds since page load, sub-millisecond precision), which is -- forwarded to the component as an action. -- -- This is the recommended driver for canvas-based animations and games -- because the browser throttles the callback to the display refresh rate -- (typically 60 fps) and pauses it automatically when the tab is hidden. -- -- = Quick start -- -- @ -- import "Miso" -- import "Miso.Subscription.RAF" -- import "Miso.Canvas" -- -- data Action = Tick Double -- DOMHighResTimeStamp in ms -- -- myComponent = ('Miso.component' model update view) -- { 'Miso.Types.subs' = [ 'rAFSub' Tick ] } -- -- update :: Action -> t'Miso.Effect.Effect' p props Model Action -- update (Tick t) = do -- 'Miso.State.modify' (\\m -> m { time = t }) -- @ -- -- = Lifecycle -- -- Internally 'rAFSub' uses 'Miso.Subscription.Util.rAFLoop' (built on -- 'Miso.Subscription.Util.createSub'): -- -- * __Acquire__ — schedules the first @requestAnimationFrame@ callback, -- which re-schedules itself on every invocation. -- * __Release__ — when the 'Sub' is stopped ('Miso.stopSub', or -- the component unmounting), the callback is told to stop; on its next -- frame it frees its JS reference instead of re-scheduling. See -- @Note [rAFLoop teardown]@ in "Miso.Subscription.Util". -- -- = See also -- -- * "Miso.Canvas" — canvas drawing API driven by 'rAFSub' ticks -- * "Miso.Subscription.Util" — 'Miso.Subscription.Util.createSub' -- * "Miso.Subscription" — re-export hub ---------------------------------------------------------------------------- module Miso.Subscription.RAF ( rAFSub , rAFSubElapsed ) where ---------------------------------------------------------------------------- import Data.IORef ---------------------------------------------------------------------------- import Miso.Effect (Sub) import Miso.Subscription.Util (rAFLoop) ---------------------------------------------------------------------------- -- | A 'Sub' for 60FPS animations when using 'requestAnimationFrame'. -- -- The 'Double' returned is a [DOMHighResTimeStamp](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp) expressed in milliseconds. -- rAFSub :: (Double -> action) -- ^ Callback fired each frame with a @DOMHighResTimeStamp@ in milliseconds -> Sub model action rAFSub :: forall action model. (Double -> action) -> Sub model action rAFSub Double -> action toAction Sink action sink = IO (Double -> IO ()) -> Sub model action forall model action. IO (Double -> IO ()) -> Sub model action rAFLoop ((Double -> IO ()) -> IO (Double -> IO ()) forall a. a -> IO a forall (f :: * -> *) a. Applicative f => a -> f a pure (Sink action sink Sink action -> (Double -> action) -> Double -> IO () forall b c a. (b -> c) -> (a -> b) -> a -> c . Double -> action toAction)) Sink action sink ---------------------------------------------------------------------------- -- | Like 'rAFSub' but fires @action@ at most once per @interval@ milliseconds. -- -- Elapsed time is accumulated in 'IORef's inside the subscription so the -- model is not touched between ticks — Miso only re-renders when a tick -- actually fires, rather than on every animation frame. -- -- @ -- app = defaultApp model update view -- { subs = [ rAFSubElapsed 175 Tick ] } -- @ -- rAFSubElapsed :: Double -- ^ Minimum interval between ticks in milliseconds (e.g. @175@ for ~6 fps) -> action -- ^ Action to dispatch each time the interval elapses -> Sub model action rAFSubElapsed :: forall action model. Double -> action -> Sub model action rAFSubElapsed Double interval action action Sink action sink = (IO (Double -> IO ()) -> Sink action -> IO model -> IO ()) -> Sink action -> IO (Double -> IO ()) -> IO model -> IO () forall a b c. (a -> b -> c) -> b -> a -> c flip IO (Double -> IO ()) -> Sink action -> IO model -> IO () forall model action. IO (Double -> IO ()) -> Sub model action rAFLoop Sink action sink (IO (Double -> IO ()) -> IO model -> IO ()) -> IO (Double -> IO ()) -> IO model -> IO () forall a b. (a -> b) -> a -> b $ do lastRef <- Double -> IO (IORef Double) forall a. a -> IO (IORef a) newIORef (Double 0 :: Double) elapRef <- newIORef (0 :: Double) pure $ \Double t -> do lastT <- IORef Double -> IO Double forall a. IORef a -> IO a readIORef IORef Double lastRef elap <- readIORef elapRef let dt = if Double lastT Double -> Double -> Bool forall a. Eq a => a -> a -> Bool == Double 0 then Double 0 else Double -> Double -> Double forall a. Ord a => a -> a -> a min Double interval (Double t Double -> Double -> Double forall a. Num a => a -> a -> a - Double lastT) newElap = Double elap Double -> Double -> Double forall a. Num a => a -> a -> a + Double dt writeIORef lastRef t if newElap >= interval then writeIORef elapRef (newElap - interval) *> sink action else writeIORef elapRef newElap ----------------------------------------------------------------------------