packages feed

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

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

Transform animation widget. Wraps a child widget whose content will be animated.
Acts as a base for most animation widgets.

Messages:

- Accepts an 'AnimationMsg', used to control the state of the animation.

@
transform t (Rect x y w h) =
  [ animTranslation $ Point tx ty
  , animScale $ Point sx sy
  ]

animTransform transform managed
@

With configuration options:

@
transform t (Rect x y w h) =
  [ animTranslation $ Point tx ty
  , animScale $ Point sx sy
  ]

animTransform_ [duration 2000, autoStart] transform managed
@

For usage examples, see:

- "Monomer.Widgets.Animation.Shake"
- "Monomer.Widgets.Animation.Zoom"
-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Strict #-}

module Monomer.Widgets.Animation.Transform (
  -- * Configuration
  TransformCfg(..),
  -- * Render transformations
  RenderTransform,
  animTranslation,
  animScale,
  animRotation,
  animGlobalAlpha,
  animScissor,
  -- * Constructors
  animTransform,
  animTransform_
) where

import Control.Applicative ((<|>))
import Control.Lens ((&), (^.), (.~))
import Control.Monad (when)
import Data.Default
import Data.Maybe
import Data.Typeable (cast)
import GHC.Generics

import qualified Data.Sequence as Seq

import Monomer.Helper
import Monomer.Widgets.Container
import Monomer.Widgets.Animation.Types

import qualified Monomer.Lens as L

{-|
Configuration options for transform:

- '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.
-}
data TransformCfg s e = TransformCfg {
  _tfcAutoStart :: Maybe Bool,
  _tfcDuration :: Maybe Millisecond,
  _tfcOnFinishedReq :: [WidgetRequest s e]
} deriving (Eq, Show)

instance Default (TransformCfg s e) where
  def = TransformCfg {
    _tfcAutoStart = Nothing,
    _tfcDuration = Nothing,
    _tfcOnFinishedReq = []
  }

instance Semigroup (TransformCfg s e) where
  (<>) tc1 tc2 = TransformCfg {
    _tfcAutoStart = _tfcAutoStart tc2 <|> _tfcAutoStart tc1,
    _tfcDuration = _tfcDuration tc2 <|> _tfcDuration tc1,
    _tfcOnFinishedReq = _tfcOnFinishedReq tc1 <> _tfcOnFinishedReq tc2
  }

instance Monoid (TransformCfg s e) where
  mempty = def

instance CmbAutoStart (TransformCfg s e) where
  autoStart_ start = def {
    _tfcAutoStart = Just start
  }

instance CmbDuration (TransformCfg s e) Millisecond where
  duration dur = def {
    _tfcDuration = Just dur
  }

instance WidgetEvent e => CmbOnFinished (TransformCfg s e) e where
  onFinished handler = def {
    _tfcOnFinishedReq = [RaiseEvent handler]
  }

instance CmbOnFinishedReq (TransformCfg s e) s e where
  onFinishedReq req = def {
    _tfcOnFinishedReq = [req]
  }

data TransformState = TransformState {
  _tfsRunning :: Bool,
  _tfsStartTs :: Millisecond
} deriving (Eq, Show, Generic)

instance Default TransformState where
  def = TransformState {
    _tfsRunning = False,
    _tfsStartTs = 0
  }

{-|
Possible render transformations:

- 'animTranslation': translates by the given offset.
- 'animScale': scales by the given size.
- 'animRotation': rotates by the given angle.
- 'animGlobalAlpha': applies the given alpha.
- 'animScissor': scissors to the given viewport.
-}
data RenderTransform = RenderTransform {
  _rtTranslation :: Maybe Point,
  _rtScale :: Maybe Point,
  _rtRotation :: Maybe Double,
  _rtGlobalAlpha :: Maybe Double,
  _rtScissor :: Maybe Rect
}

instance Default RenderTransform where
  def = RenderTransform {
    _rtTranslation = Nothing,
    _rtScale = Nothing,
    _rtRotation = Nothing,
    _rtGlobalAlpha = Nothing,
    _rtScissor = Nothing
  }

instance Semigroup RenderTransform where
  (<>) rt1 rt2 = RenderTransform {
    _rtTranslation = _rtTranslation rt2 <|> _rtTranslation rt1,
    _rtScale = _rtScale rt2 <|> _rtScale rt1,
    _rtRotation = _rtRotation rt2 <|> _rtRotation rt1,
    _rtGlobalAlpha = _rtGlobalAlpha rt2 <|> _rtGlobalAlpha rt1,
    _rtScissor = _rtScissor rt2 <|> _rtScissor rt1
  }

instance Monoid RenderTransform where
  mempty = def

-- | Translate by the given offset.
animTranslation :: Point -> RenderTransform
animTranslation p = def { _rtTranslation = Just p }

-- | Scale by the given size.
animScale :: Point -> RenderTransform
animScale p = def { _rtScale = Just p }

-- | Rotate by the given angle.
animRotation :: Double -> RenderTransform
animRotation r = def { _rtRotation = Just r }

-- | Apply the given alpha.
animGlobalAlpha :: Double -> RenderTransform
animGlobalAlpha a = def { _rtGlobalAlpha = Just a }

-- | Scissor to the given viewport.
animScissor :: Rect -> RenderTransform
animScissor vp = def { _rtScissor = Just vp }

type Transformer = (Double -> Rect -> [RenderTransform])

-- | Animates a widget through translation, scaling, rotation,
--   transparency and scissor.
animTransform
  :: WidgetEvent e
  => Transformer     -- ^ Transformations from time (in ms) and viewport.
  -> WidgetNode s e  -- ^ The child node.
  -> WidgetNode s e  -- ^ The created animation container.
animTransform f managed = animTransform_ def f managed

-- | Animates a widget through translation, scaling, rotation,
--   transparency and scissor. Accepts config.
animTransform_
  :: WidgetEvent e
  => [TransformCfg s e]  -- ^ The config options.
  -> Transformer       -- ^ Transformations from time (in ms) and viewport.
  -> WidgetNode s e    -- ^ The child node.
  -> WidgetNode s e    -- ^ The created animation container.
animTransform_ configs f managed = node where
  node = defaultWidgetNode widgetType widget
    & L.info . L.focusable .~ False
    & L.children .~ Seq.singleton managed
  widgetType = WidgetType "animTransform"
  widget = makeTransform f config def
  config = mconcat configs

makeTransform
  :: WidgetEvent e
  => Transformer
  -> TransformCfg s e
  -> TransformState
  -> Widget s e
makeTransform f config state = widget where
  baseWidget = createContainer state def {
    containerInit = init,
    containerMerge = merge,
    containerHandleMessage = handleMessage
  }
  widget = baseWidget {
    widgetRender = render
  }

  TransformCfg{..} = config
  TransformState{..} = state
  autoStart = fromMaybe False _tfcAutoStart
  duration = fromMaybe 500 _tfcDuration
  period = 20
  steps = fromIntegral $ duration `div` period

  finishedReq node ts = delayedMessage node (AnimationFinished ts) duration
  renderReq wenv node = req where
    widgetId = node ^. L.info . L.widgetId
    req = RenderEvery widgetId period (Just steps)

  init wenv node = result where
    ts = wenv ^. L.timestamp
    newNode = node
      & L.widget .~ makeTransform f config (TransformState True ts)
    result
      | autoStart = resultReqs newNode [finishedReq node ts, renderReq wenv node]
      | otherwise = resultNode node

  merge wenv node oldNode oldState = resultNode newNode where
    newNode = node
      & L.widget .~ makeTransform f config oldState

  handleMessage wenv node target message = result where
    result = cast message >>= Just . handleAnimateMsg wenv node

  handleAnimateMsg wenv node msg = result where
    widgetId = node ^. L.info . L.widgetId
    ts = wenv ^. L.timestamp
    startState = TransformState True ts
    startReqs = [finishedReq node ts, renderReq wenv node]

    newNode newState = node
      & L.widget .~ makeTransform f config newState
    result = case msg of
      AnimationStart -> resultReqs (newNode startState) startReqs
      AnimationStop -> resultReqs (newNode def) [RenderStop widgetId]
      AnimationFinished ts'
        | isRelevant -> resultReqs node _tfcOnFinishedReq
        | otherwise -> resultNode node
        where isRelevant = _tfsRunning && ts' == _tfsStartTs

  render wenv node renderer = do
    if _tfsRunning
      then createOverlay renderer $ do
        saveContext renderer
        setTranslation renderer $ wenv ^. L.offset
        intersectScissor renderer scissorViewport
        setTranslation renderer $ Point (x+w/2) (y+h/2)
        setRotation renderer rotation
        setTranslation renderer $ Point (-x-w/2) (-y-h/2)
        setTranslation renderer $ Point (tx+x*(1-sx)) (ty+y*(1-sy))
        setScale renderer scale
        setGlobalAlpha renderer alpha
        widgetRender (cnode ^. L.widget) wenv cnode renderer
        restoreContext renderer
      else widgetRender (cnode ^. L.widget) wenv cnode renderer
    where
      vp@(Rect x y w h) = node ^. L.info . L.viewport
      t = clamp 0 duration $ (wenv ^. L.timestamp) - _tfsStartTs
      RenderTransform{..} = mconcat $ f (fromIntegral t) vp
      Point tx ty = fromMaybe (Point 0 0) _rtTranslation
      scale@(Point sx sy) = fromMaybe (Point 1 1) _rtScale
      rotation = fromMaybe 0 _rtRotation
      alpha = fromMaybe 1 _rtGlobalAlpha
      scissorViewport = fromMaybe vp _rtScissor
      cnode = Seq.index (node ^. L.children) 0