packages feed

nano-ui-0.1.0.0: lib/NanoUI/Widgets/Animate.hs

module NanoUI.Widgets.Animate
  ( Transition (..)
  , animate
  , animateTo
  , animateToA
  , pulse
  , keepAnimating
  )
where

import Control.Monad (when)
import Data.Maybe (isNothing)
import Effectful (Eff, type (:>))
import NanoUI.Animatable (Animatable (..))
import NanoUI.Animation (SpringParams)
import NanoUI.Context
  ( Ease (..)
  , approxEq
  , easeSameSpec
  , getAnimationValue
  , lookupAnimation
  , setAnimationValue
  , startAnimation
  , startAnimationEaseDelay
  , startSpring
  )
import NanoUI.Monad (Ui, askContext, nextId, scope, uiIO, uiTime, withKey)
import NanoUI.Widgets.Node (HasResponse, respId)

-- | How an animated value moves.
data Transition
  = -- | Eased tween: duration and start delay, in seconds.
    Tween !Ease !Float !Float
  | -- | Damped spring; retargets from its current position and velocity.
    Spring !SpringParams

-- | Animate from @from@ to @to@. It starts over from @from@ once it has
-- finished (a tween completes, a spring settles) or its tween changes, so
-- calling it every frame cycles.
animate :: Ui :> es => Transition -> Float -> Float -> Eff es Float
animate transition from to = do
  wid <- nextId
  ctx <- askContext
  uiIO $ do
    case transition of
      Tween ease dur delay -> startAnimationEaseDelay ctx wid from to dur ease delay
      Spring params -> do
        running <- lookupAnimation ctx wid
        when (isNothing running) (setAnimationValue ctx wid from)
        startSpring ctx wid params to
    getAnimationValue ctx wid

-- | Animate from the current value toward @target@. An unchanged target keeps
-- the running animation; a new one retargets from wherever the value is.
animateTo :: Ui :> es => Transition -> Float -> Eff es Float
animateTo transition target = do
  wid <- nextId
  ctx <- askContext
  uiIO $ do
    case transition of
      Tween ease dur delay -> do
        cur <- getAnimationValue ctx wid
        manim <- lookupAnimation ctx wid
        case manim of
          Just a | easeSameSpec a ease dur delay target -> pure ()
          Nothing | approxEq cur target -> pure ()
          _ -> startAnimationEaseDelay ctx wid cur target dur ease delay
      Spring params -> startSpring ctx wid params target
    getAnimationValue ctx wid

-- | 'animateTo' for every component of a composite value.
animateToA :: (Animatable a, Ui :> es) => Transition -> a -> Eff es a
animateToA transition = animateComponents (animateTo transition)

-- Component keys are local to one composite value, not its parent widget.
animateComponents ::
  (Animatable a, Ui :> es) => (Float -> Eff es Float) -> a -> Eff es a
animateComponents animateComponent target = scope $ do
  components <-
    mapM
      (\(index, value) -> withKey (index :: Int) (animateComponent value))
      (zip [0 ..] (toComponents target))
  pure (fromComponents components)

-- | A smoothly oscillating value in @[0,1]@ driven by the real-time clock, with
-- the given period in seconds (e.g. @pulse 6@ sweeps once every six seconds).
-- The time is captured in 'Double' (see 'NanoUI.Monad.uiTime'), so the sweep
-- stays sub-frame smooth even on long-running processes. The value is
-- re-evaluated each frame, like 'animate'.
pulse :: Ui :> es => Float -> Eff es Float
pulse periodSec = do
  t <- uiTime
  let
    period = max 0.001 (realToFrac periodSec :: Double)
  pure (realToFrac (0.5 + 0.5 * sin (2 * pi * t / period)) :: Float)

-- | Keep a widget animating indefinitely so the frame loop never idles. Widgets
-- driven by the wall clock ('pulse', or drawing from 'NanoUI.Monad.uiTime')
-- rather than by a frame-counted animation would otherwise stop repainting
-- once other animations settle.
--
-- > bar <- progressBar' =<< pulse 6
-- > keepAnimating bar
keepAnimating :: (HasResponse r, Ui :> es) => r -> Eff es ()
keepAnimating resp = do
  ctx <- askContext
  uiIO (startAnimation ctx (respId resp) 0 1 1e9)