{-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE NoImplicitPrelude #-} module Imj.Graphics.Interpolation.Evolution ( -- * Evolution {- | 'Evolution' is a helper type to interpolate between 'DiscreteInterpolation's or morph between 'DiscreteMorphing'. It stores the 'distance' to cache potential expensive distance computations. The preferred way to create it is to use 'mkEvolutionEaseQuart' which uses the inverse ease function 'invQuartEaseInOut'. To produce the desired /easing/ effect, the 'Evolution' should be updated at specific time intervals. In that respect, 'getDeltaTimeToNextFrame' computes the next time at which the interpolation should be updated (for interpolations) or rendered (for morphings), based on the current frame and the inverse ease function. -} Evolution(..) , mkEvolutionEaseQuart , mkEvolution , getDeltaTimeToNextFrame -- ** Getting an interpolated value , getValueAt -- ** Draw a morphing , drawMorphingAt -- ** Synchronizing multiple Evolutions -- | 'EaseClock' can be used to synchronize multiple 'Evolution's. , EaseClock(..) , mkEaseClock ) where import GHC.Show(showString) import Imj.Prelude import Control.Monad.IO.Class(MonadIO) import Control.Monad.Reader.Class(MonadReader) import Imj.Graphics.Class.DiscreteInterpolation import Imj.Graphics.Class.DiscreteMorphing import Imj.Graphics.Math.Ease import Imj.Iteration {-# INLINABLE mkEvolutionEaseQuart #-} -- | An evolution between n 'DiscreteDistance's. With a 4th order ease in & out. mkEvolutionEaseQuart :: DiscreteDistance v => Successive v -- ^ 'DiscreteDistance's through which the evolution will pass. -> Float -- ^ Duration in seconds -> Evolution v mkEvolutionEaseQuart = mkEvolution invQuartEaseInOut -- | An evolution between n 'DiscreteDistance's. With a user-specified (inverse) ease function. {-# INLINABLE mkEvolution #-} mkEvolution :: DiscreteDistance v => (Float -> Float) -- ^ Inverse continuous ease function -> Successive v -- ^ 'DiscreteDistance's through which the evolution will pass. -> Float -- ^ Duration in seconds -> Evolution v mkEvolution ease s duration = let nSteps = distanceSuccessive s lastFrame = Frame $ pred nSteps in Evolution s lastFrame duration (discreteAdaptor ease nSteps) -- | Used to synchronize multiple 'Evolution's. newtype EaseClock = EaseClock (Evolution NotWaypoint) deriving (Show) newtype NotWaypoint = NotWaypoint () deriving(Show) -- | To make sure that we never use distance on an 'EaseClock'. instance DiscreteDistance NotWaypoint where distance = error "don't use distance on NotWaypoint" -- | Constructor of 'EaseClock' mkEaseClock :: Float -- ^ Duration in seconds -> Frame -- ^ Last frame -> (Float -> Float) -- ^ Inverse ease function (value -> time, both between 0 and 1) -> EaseClock mkEaseClock duration lastFrame ease = let nSteps = fromIntegral $ succ lastFrame in EaseClock $ Evolution (Successive []) lastFrame duration (discreteAdaptor ease nSteps) -- TODO we could optimize by precomputing the lastframes of each individual segment, -- and select the interval without having to recompute every distance. -- We could change the Successive type to store the cumulated distance, -- then do a binary search -- | Defines an evolution (interpolation or morphing) between 'Successive' 'DiscreteDistance's. data Evolution v = Evolution { _evolutionSuccessive :: !(Successive v) -- ^ 'Successive' 'DiscreteDistance's. , _evolutionLastFrame :: !Frame -- ^ The frame at which the 'Evolution' value is equal to the last 'Successive' value. , _evolutionDuration :: Float -- ^ Duration of the interpolation in seconds. , _evolutionInverseEase :: Float -> Float -- ^ Inverse ease function. } instance (Show v) => Show (Evolution v) where showsPrec _ (Evolution a b c _) = showString $ "Evolution{" ++ show a ++ show b ++ show c ++ "}" -- | Computes the time increment between the input 'Frame' and the next. getDeltaTimeToNextFrame :: Evolution v -> Frame -> Maybe Float -- ^ If evolution is still ongoing, returns the time interval -- between the input 'Frame' and the next. getDeltaTimeToNextFrame (Evolution _ lastFrame@(Frame lastStep) duration easeValToTime) frame@(Frame step) | frame < 0 = error "negative frame" | frame >= lastFrame = Nothing | otherwise = Just dt where nextStep = succ step thisValue = fromIntegral step / fromIntegral lastStep targetValue = fromIntegral nextStep / fromIntegral lastStep dt = duration * (easeValToTime targetValue - easeValToTime thisValue) {-# INLINABLE getValueAt #-} -- | Gets the value of an 'Evolution' at a given 'Frame'. getValueAt :: DiscreteInterpolation v => Evolution v -> Frame -> v -- ^ The evolution value. getValueAt (Evolution s@(Successive l) lastFrame _ _) frame@(Frame step) | frame <= 0 = head l | frame >= lastFrame = last l | otherwise = interpolateSuccessive s step {-# INLINABLE drawMorphingAt #-} -- | Draws an 'Evolution' at a given 'Frame'. drawMorphingAt :: (DiscreteMorphing v, Draw e, MonadReader e m, MonadIO m) => Evolution v -> Frame -> m () drawMorphingAt (Evolution s _ _ _) (Frame step) = drawMorphingSuccessive s $ assert (step >= 0) step