packages feed

monomer-1.6.0.0: src/Monomer/Widgets/Animation/Shake.hs

{-|
Module      : Monomer.Widgets.Animation.Shake
Copyright   : (c) 2023 Ruslan Gadeev, Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Shake animation widget. Wraps a child widget whose content will be animated.

Messages:

- Accepts a 'AnimationMsg', used to control the state of the animation.
-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Strict #-}

module Monomer.Widgets.Animation.Shake (
  -- * Configuration
  ShakeCfg,
  shakeH,
  shakeV,
  shakeR,
  shakeS,
  shakeAmplitude,
  shakeFrequency,
  -- * Constructors
  animShake,
  animShake_
) where

import Control.Applicative ((<|>))
import Control.Lens ((&), (.~))
import Data.Default
import Data.Maybe

import Monomer.Helper
import Monomer.Widgets.Container
import Monomer.Widgets.Animation.Transform

import qualified Monomer.Lens as L

data ShakeDirection
  = ShakeH
  | ShakeV
  | ShakeR
  | ShakeS
  deriving (Eq, Show)

{-|
Configuration options for shake:

- 'autoStart': whether the first time the widget is added, animation should run.
- 'duration': how long the animation lasts in ms.
- 'onFinished': event to raise when animation is complete.
- 'onFinishedReq': 'WidgetRequest' to generate when animation is complete.
- 'shakeAmplitude': amplitude of the animation. Defaults to 0.1.
- 'shakeFrequency': frequency of the animation. Defaults to 2.
- Individual combinators for direction.
-}
data ShakeCfg s e = ShakeCfg {
  _shcDirection :: Maybe ShakeDirection,
  _shcAmplitude :: Maybe Double,
  _shcFrequency :: Maybe Int,
  _shcTransformCfg :: TransformCfg s e
} deriving (Eq, Show)

instance Default (ShakeCfg s e) where
  def = ShakeCfg {
    _shcDirection = Nothing,
    _shcAmplitude = Nothing,
    _shcFrequency = Nothing,
    _shcTransformCfg = def
  }

instance Semigroup (ShakeCfg s e) where
  (<>) sc1 sc2 = ShakeCfg {
    _shcDirection = _shcDirection sc2 <|> _shcDirection sc1,
    _shcAmplitude = _shcAmplitude sc2 <|> _shcAmplitude sc1,
    _shcFrequency = _shcFrequency sc2 <|> _shcFrequency sc1,
    _shcTransformCfg = _shcTransformCfg sc1 <> _shcTransformCfg sc2
  }

instance Monoid (ShakeCfg s e) where
  mempty = def

instance CmbAutoStart (ShakeCfg s e) where
  autoStart_ start = def {
    _shcTransformCfg = autoStart_ start
  }

instance CmbDuration (ShakeCfg s e) Millisecond where
  duration dur = def {
    _shcTransformCfg = duration dur
  }

instance WidgetEvent e => CmbOnFinished (ShakeCfg s e) e where
  onFinished handler = def {
    _shcTransformCfg = onFinished handler
  }

instance CmbOnFinishedReq (ShakeCfg s e) s e where
  onFinishedReq req = def {
    _shcTransformCfg = onFinishedReq req
  }

-- | Shake horizontally.
shakeH :: ShakeCfg s e
shakeH = def { _shcDirection = Just ShakeH }

-- | Shake vertically.
shakeV :: ShakeCfg s e
shakeV = def { _shcDirection = Just ShakeV }

-- | Shake by rotating.
shakeR :: ShakeCfg s e
shakeR = def { _shcDirection = Just ShakeR }

-- | Shake by scaling.
shakeS :: ShakeCfg s e
shakeS = def { _shcDirection = Just ShakeS }

-- | Amplitude of the animation. Defaults to 1.
shakeAmplitude :: Double -> ShakeCfg s e
shakeAmplitude amp = def { _shcAmplitude = Just amp }

-- | Frequency of the animation. Defaults to 2.
shakeFrequency :: Int -> ShakeCfg s e
shakeFrequency freq = def { _shcFrequency = Just freq }

-- | Shakes a widget.
animShake
  :: WidgetEvent e
  => WidgetNode s e  -- ^ The child node.
  -> WidgetNode s e  -- ^ The created animation container.
animShake managed = animShake_ def managed

-- | Shakes a widget. Accepts config.
animShake_
  :: WidgetEvent e
  => [ShakeCfg s e]    -- ^ The config options.
  -> WidgetNode s e  -- ^ The child node.
  -> WidgetNode s e  -- ^ The created animation container.
animShake_ configs managed = node where
  node = animTransform_ [_shcTransformCfg] f managed
    & L.info . L.widgetType .~ "animShake"
  f t vp@(Rect _ _ w h) = (noScissor vp) <> case dir of
    ShakeH -> [animTranslation $ Point ((step t)*w) 0]
    ShakeV -> [animTranslation $ Point 0 ((step t)*h)]
    ShakeR -> [animRotation $ (step t)*180]
    ShakeS ->
      [ animTranslation $ Point ((1-(ss t))*w/2) ((1-(ss t))*h/2)
      , animScale $ Point (ss t) (ss t)
      ]
  noScissor (Rect x y w h) =
    [animScissor $ Rect (x-w*10) (y-h*10) (w*20) (h*20)]
  step t = (sin $ (fs t)*freq*2*pi)*amp
  ss t = 1-(amp/2)+(cos $ (fs t)*freq*2*pi)*amp/2
  fs t = clamp 0 1 $ t/(fromIntegral dur)
  dir = fromMaybe ShakeH _shcDirection
  amp = fromMaybe 0.1 _shcAmplitude
  freq = fromIntegral $ fromMaybe 2 _shcFrequency
  dur = fromMaybe 500 _tfcDuration
  TransformCfg{..} = _shcTransformCfg
  ShakeCfg{..} = mconcat configs