{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE RecordWildCards #-} module Reanimate.Scene.Object where import Control.Lens import Control.Monad (forM_, void) import Control.Monad.State (State, execState) import Data.Monoid (Last (getLast)) import Graphics.SvgTree (Number (..), Tree, pattern None, strokeWidth, toUserUnit) import Linear.V2 (R1 (_x), R2 (_y), V2 (..)) import Linear.Vector (Additive (lerp), (^*)) import Reanimate.Animation import Reanimate.Constants (defaultDPI, defaultStrokeWidth) import Reanimate.Ease (Signal, curveS, fromToS) import Reanimate.Effect (applyE, fadeLineOutE, overEnding) import Reanimate.Math.Balloon (balloon) import Reanimate.Morph.Common (morph) import Reanimate.Morph.Linear (linear) import Reanimate.Svg import Reanimate.Scene.Core (Scene, fork, scene, wait) import Reanimate.Scene.Sprite (Sprite, newSprite, newSpriteA', play, spriteModify, unVar) import Reanimate.Scene.Var (Var, modifyVar, newVar, readVar, tweenVar) ------------------------------------------------------- -- Objects -- | Objects can be any Haskell structure as long as it can be rendered to SVG. class Renderable a where toSVG :: a -> SVG instance Renderable Tree where toSVG = id -- | Objects are SVG nodes (represented as Haskell values) with -- identity, location, and several other properties that can -- change over time. data Object s a = Object { objectSprite :: Sprite s, objectData :: Var s (ObjectData a) } -- | Container for object properties. data ObjectData a = ObjectData { _oTranslate :: V2 Double, _oValueRef :: a, _oSVG :: SVG, _oContext :: SVG -> SVG, -- | Top, right, bottom, left _oMargin :: (Double, Double, Double, Double), _oBB :: (Double, Double, Double, Double), _oOpacity :: Double, _oShown :: Bool, _oZIndex :: Int, _oEasing :: Signal, _oScale :: Double, _oScaleOrigin :: V2 Double } -- Basic lenses -- FIXME: Maybe 'position' is a better name. -- | Object position. Default: \<0,0\> oTranslate :: Lens' (ObjectData a) (V2 Double) oTranslate = lens _oTranslate $ \obj val -> obj {_oTranslate = val} -- | Object X position. Default: 0 oTranslateX :: Lens' (ObjectData a) Double oTranslateX = oTranslate . _x -- | Object Y position. Default: 0 oTranslateY :: Lens' (ObjectData a) Double oTranslateY = oTranslate . _y -- | Rendered SVG node of an object. Does not include context -- or object properties. Read-only. oSVG :: Getter (ObjectData a) SVG oSVG = to _oSVG -- | Custom render context. Is applied to the object for every -- frame that it is shown. oContext :: Lens' (ObjectData a) (SVG -> SVG) oContext = lens _oContext $ \obj val -> obj {_oContext = val} -- | Object margins (top, right, bottom, left) in local units. oMargin :: Lens' (ObjectData a) (Double, Double, Double, Double) oMargin = lens _oMargin $ \obj val -> obj {_oMargin = val} -- | Object bounding-box (minimal X-coordinate, minimal Y-coordinate, -- width, height). Uses `Reanimate.Svg.BoundingBox.boundingBox` -- and has the same limitations. oBB :: Getter (ObjectData a) (Double, Double, Double, Double) oBB = to _oBB -- | Object opacity. Default: 1 oOpacity :: Lens' (ObjectData a) Double oOpacity = lens _oOpacity $ \obj val -> obj {_oOpacity = val} -- | Toggle for whether or not the object should be rendered. -- Default: False oShown :: Lens' (ObjectData a) Bool oShown = lens _oShown $ \obj val -> obj {_oShown = val} -- | Object's z-index. oZIndex :: Lens' (ObjectData a) Int oZIndex = lens _oZIndex $ \obj val -> obj {_oZIndex = val} -- | Easing function used when modifying object properties. -- Default: @'Reanimate.Ease.curveS' 2@ oEasing :: Lens' (ObjectData a) Signal oEasing = lens _oEasing $ \obj val -> obj {_oEasing = val} -- | Object's scale. Default: 1 oScale :: Lens' (ObjectData a) Double oScale = lens _oScale $ \obj val -> oComputeBB obj {_oScale = val} -- | Origin point for scaling. Default: \<0,0\> oScaleOrigin :: Lens' (ObjectData a) (V2 Double) oScaleOrigin = lens _oScaleOrigin $ \obj val -> oComputeBB obj {_oScaleOrigin = val} -- Smart lenses -- | Lens for the source value contained in an object. oValue :: Renderable a => Lens' (ObjectData a) a oValue = lens _oValueRef $ \obj newVal -> let svg = toSVG newVal in oComputeBB obj { _oValueRef = newVal, _oSVG = svg } oComputeBB :: ObjectData a -> ObjectData a oComputeBB obj = obj { _oBB = boundingBox $ oScaleApply obj (_oSVG obj) } -- | Derived location of the top-most point of an object + margin. oTopY :: Lens' (ObjectData a) Double oTopY = lens getter setter where getter obj = let top = obj ^. oMarginTop miny = obj ^. oBBMinY h = obj ^. oBBHeight dy = obj ^. oTranslate . _2 in dy + miny + h + top setter obj val = obj & (oTranslate . _2) +~ val - getter obj -- | Derived location of the bottom-most point of an object + margin. oBottomY :: Lens' (ObjectData a) Double oBottomY = lens getter setter where getter obj = let bot = obj ^. oMarginBottom miny = obj ^. oBBMinY dy = obj ^. oTranslate . _2 in dy + miny - bot setter obj val = obj & (oTranslate . _2) +~ val - getter obj -- | Derived location of the left-most point of an object + margin. oLeftX :: Lens' (ObjectData a) Double oLeftX = lens getter setter where getter obj = let left = obj ^. oMarginLeft minx = obj ^. oBBMinX dx = obj ^. oTranslate . _1 in dx + minx - left setter obj val = obj & (oTranslate . _1) +~ val - getter obj -- | Derived location of the right-most point of an object + margin. oRightX :: Lens' (ObjectData a) Double oRightX = lens getter setter where getter obj = let right = obj ^. oMarginRight minx = obj ^. oBBMinX w = obj ^. oBBWidth dx = obj ^. oTranslate . _1 in dx + minx + w + right setter obj val = obj & (oTranslate . _1) +~ val - getter obj -- | Derived location of an object's center point. oCenterXY :: Lens' (ObjectData a) (V2 Double) oCenterXY = lens getter setter where getter obj = let minx = obj ^. oBBMinX miny = obj ^. oBBMinY w = obj ^. oBBWidth h = obj ^. oBBHeight V2 dx dy = obj ^. oTranslate in V2 (dx + minx + w / 2) (dy + miny + h / 2) setter obj (V2 dx dy) = let V2 x y = getter obj in obj & (oTranslate . _1) +~ dx - x & (oTranslate . _2) +~ dy - y -- | Derived location of an object's center X value. oCenterX :: Lens' (ObjectData a) Double oCenterX = oCenterXY . _x -- | Derived location of an object's center Y value. oCenterY :: Lens' (ObjectData a) Double oCenterY = oCenterXY . _y -- | Object's top margin. oMarginTop :: Lens' (ObjectData a) Double oMarginTop = oMargin . _1 -- | Object's right margin. oMarginRight :: Lens' (ObjectData a) Double oMarginRight = oMargin . _2 -- | Object's bottom margin. oMarginBottom :: Lens' (ObjectData a) Double oMarginBottom = oMargin . _3 -- | Object's left margin. oMarginLeft :: Lens' (ObjectData a) Double oMarginLeft = oMargin . _4 -- | Object's minimal X-coordinate.. oBBMinX :: Getter (ObjectData a) Double oBBMinX = oBB . _1 -- | Object's minimal Y-coordinate.. oBBMinY :: Getter (ObjectData a) Double oBBMinY = oBB . _2 -- | Object's width without margin. oBBWidth :: Getter (ObjectData a) Double oBBWidth = oBB . _3 -- | Object's height without margin. oBBHeight :: Getter (ObjectData a) Double oBBHeight = oBB . _4 ------------------------------------------------------------------------------- -- Object modifiers -- | Modify object properties. oModify :: Object s a -> (ObjectData a -> ObjectData a) -> Scene s () oModify o = modifyVar (objectData o) -- | Modify object properties using a stateful API. oModifyS :: Object s a -> State (ObjectData a) b -> Scene s () oModifyS o = oModify o . execState -- | Query object property. oRead :: Object s a -> Getting b (ObjectData a) b -> Scene s b oRead o l = view l <$> readVar (objectData o) -- | Modify object properties over a set duration. oTween :: Object s a -> Duration -> (Double -> ObjectData a -> ObjectData a) -> Scene s () oTween o d fn = do -- Read 'easing' var here instead of taking it from 'v'. -- This allows different easing functions even at the same timestamp. ease <- oRead o oEasing tweenVar (objectData o) d (\v t -> fn (ease t) v) -- | Modify object properties over a set duration using a stateful API. oTweenS :: Object s a -> Duration -> (Double -> State (ObjectData a) b) -> Scene s () oTweenS o d fn = oTween o d (execState . fn) -- | Modify object value over a set duration. This is a convenience function -- for modifying `oValue`. oTweenV :: Renderable a => Object s a -> Duration -> (Double -> a -> a) -> Scene s () oTweenV o d fn = oTween o d (\t -> oValue %~ fn t) -- | Modify object value over a set duration using a stateful API. This is a -- convenience function for modifying `oValue`. oTweenVS :: Renderable a => Object s a -> Duration -> (Double -> State a b) -> Scene s () oTweenVS o d fn = oTween o d (\t -> oValue %~ execState (fn t)) -- | Create new object. oNew :: Renderable a => a -> Scene s (Object s a) oNew = newObject -- | Create new object. newObject :: Renderable a => a -> Scene s (Object s a) newObject val = do ref <- newVar ObjectData { _oTranslate = V2 0 0, _oValueRef = val, _oSVG = svg, _oContext = id, _oMargin = (0.5, 0.5, 0.5, 0.5), _oBB = boundingBox svg, _oOpacity = 1, _oShown = False, _oZIndex = 1, _oEasing = curveS 2, _oScale = 1, _oScaleOrigin = V2 0 0 } sprite <- newSprite $ do ~obj@ObjectData {..} <- unVar ref pure $ if _oShown then uncurryV2 translate _oTranslate $ oScaleApply obj $ withGroupOpacity _oOpacity $ mkGroup [_oContext _oSVG] else None spriteModify sprite $ do ~ObjectData {_oZIndex = z} <- unVar ref pure $ \(img, _) -> (img, z) return Object { objectSprite = sprite, objectData = ref } where svg = toSVG val oScaleApply :: ObjectData a -> (SVG -> SVG) oScaleApply ObjectData {..} = uncurryV2 translate (negate _oScaleOrigin) . scale _oScale . uncurryV2 translate _oScaleOrigin uncurryV2 :: (a -> a -> b) -> V2 a -> b uncurryV2 fn (V2 a b) = fn a b ------------------------------------------------------------------------------- -- Graphical transformations -- | Instantly show object. oShow :: Object s a -> Scene s () oShow o = oModify o $ oShown .~ True -- | Instantly hide object. oHide :: Object s a -> Scene s () oHide o = oModify o $ oShown .~ False -- | Show object with an animator function. The animator is responsible for -- transitioning the object from invisible to having its final shape. -- If this doesn't hold true for the animator function then the final -- animation will be discontinuous. oShowWith :: Object s a -> (SVG -> Animation) -> Scene s () oShowWith o fn = do oModify o $ oShown .~ True initSVG <- oRead o oSVG let ani = fn initSVG oTween o (duration ani) $ \t obj -> obj {_oSVG = getAnimationFrame SyncStretch ani t 1} oModify o $ \obj -> obj {_oSVG = initSVG} -- | Hide object with an animator function. The animator is responsible for -- transitioning the object from visible to invisible. -- If this doesn't hold true for the animator function then the final -- animation will be discontinuous. oHideWith :: Object s a -> (SVG -> Animation) -> Scene s () oHideWith o fn = do initSVG <- oRead o oSVG let ani = fn initSVG oTween o (duration ani) $ \t obj -> obj {_oSVG = getAnimationFrame SyncStretch ani t 1} oModify o $ \obj -> obj {_oSVG = initSVG} oModify o $ oShown .~ False -- | Fade in object over a set duration. oFadeIn :: SVG -> Animation oFadeIn svg = animate $ \t -> withGroupOpacity t svg -- | Fade out object over a set duration. oFadeOut :: SVG -> Animation oFadeOut = reverseA . oFadeIn -- | Scale in object over a set duration. -- -- Example: -- -- @ -- do txt <- 'oNew' $ 'withStrokeWidth' 0 $ 'withFillOpacity' 1 $ -- 'center' $ 'scale' 3 $ 'Reanimate.LaTeX.latex' "oGrow" -- 'oShowWith' txt 'oGrow' -- 'wait' 1; 'oHideWith' txt 'oFadeOut' -- @ -- -- <> oGrow :: SVG -> Animation oGrow svg = animate $ \t -> scale t svg -- | Scale out object over a set duration. oShrink :: SVG -> Animation oShrink = reverseA . oGrow -- | Relative coordinates for an SVG node. type Origin = (Double, Double) svgOrigin :: SVG -> Origin -> (Double, Double) svgOrigin svg (originX, originY) = case boundingBox svg of (polyX, polyY, polyWidth, polyHeight) -> ( polyX + polyWidth * originX, polyY + polyHeight * originY ) -- | Scale in children from left to right, with an origin at the top of each child. -- -- Example: -- -- @ -- do txt <- 'oNew' $ 'withStrokeWidth' 0 $ 'withFillOpacity' 1 $ -- 'center' $ 'scale' 3 $ 'Reanimate.LaTeX.latex' "oScaleIn" -- 'oShowWith' txt $ 'adjustDuration' (*2) . 'oScaleIn' -- 'wait' 1; 'oHideWith' txt 'oFadeOut' -- @ -- -- <> oScaleIn :: SVG -> Animation oScaleIn = oScaleIn' (curveS 2) (0.5, 1) -- | Like 'oScaleIn' but takes an easing function and an origin. oScaleIn' :: Signal -> Origin -> SVG -> Animation oScaleIn' easing origin = oStagger' 0.05 $ \svg -> let (cx, cy) = svgOrigin svg origin in signalA easing $ mkAnimation 0.3 $ \t -> translate cx cy $ scale t $ translate (- cx) (- cy) svg -- | Scale out children from left to right, with an origin at the bottom of each child. -- -- Example: -- -- @ -- do txt <- 'oNew' $ 'withStrokeWidth' 0 $ 'withFillOpacity' 1 $ -- 'center' $ 'scale' 3 $ 'Reanimate.LaTeX.latex' "oScaleOut" -- 'oShowWith' txt 'oFadeIn' -- 'oHideWith' txt $ 'adjustDuration' (*2) . 'oScaleOut' -- @ -- -- <> oScaleOut :: SVG -> Animation oScaleOut = reverseA . oStaggerRev' 0.05 (oScaleIn' (curveS 2) (0.5, 0)) -- | Like 'oScaleOut' but takes an easing function and an origin. oScaleOut' :: Signal -> Origin -> SVG -> Animation oScaleOut' easing origin = reverseA . oStaggerRev' 0.05 (oScaleIn' easing origin) -- | Animate each child node in parallel. oSim :: (SVG -> Animation) -> SVG -> Animation oSim = oStagger' 0 -- oSim (oStagger fn) = oSim fn -- oStagger (oStagger fn) = oStagger fn -- | Animate each child node in parallel, staggered by 0.2 seconds. oStagger :: (SVG -> Animation) -> SVG -> Animation oStagger = oStagger' 0.2 -- | Animate each child node in parallel, staggered by 0.2 seconds and in reverse order. oStaggerRev :: (SVG -> Animation) -> SVG -> Animation oStaggerRev = oStaggerRev' 0.2 -- | Animate each child node in parallel, staggered by a given duration. oStagger' :: Duration -> (SVG -> Animation) -> SVG -> Animation oStagger' staggerDelay fn svg = scene $ forM_ (svgGlyphs svg) $ \(ctx, _attr, node) -> do void $ fork $ newSpriteA' SyncFreeze (fn $ ctx node) wait staggerDelay -- | Animate each child node in parallel, staggered by given duration and in reverse order. oStaggerRev' :: Duration -> (SVG -> Animation) -> SVG -> Animation oStaggerRev' staggerDelay fn svg = scene $ forM_ (reverse $ svgGlyphs svg) $ \(ctx, _attr, node) -> do void $ fork $ newSpriteA' SyncFreeze (fn $ ctx node) wait staggerDelay -- | Render SVG by first drawing outlines and then filling shapes. -- -- Example: -- -- @ -- do txt <- 'oNew' $ 'withStrokeWidth' 0 $ 'withFillOpacity' 1 $ -- 'center' $ 'scale' 4 $ 'Reanimate.LaTeX.latex' "oDraw" -- 'oModify' txt $ 'oEasing' .~ id -- 'oShowWith' txt 'oDraw'; 'wait' 1 -- 'oHideWith' txt 'oFadeOut' -- @ -- -- <> oDraw :: SVG -> Animation oDraw = oStagger $ \svg -> scene $ forM_ (svgGlyphs $ pathify svg) $ \(ctx, attr, node) -> do let sWidth = case toUserUnit defaultDPI <$> getLast (attr ^. strokeWidth) of Just (Num d) -> max defaultStrokeWidth d _ -> defaultStrokeWidth -- wait 1 play $ mapA ctx $ applyE (overEnding fillDur $ fadeLineOutE sWidth) $ animate $ \t -> withStrokeWidth sWidth $ mkGroup [withFillOpacity 0 $ partialSvg t node] wait (- fillDur) newSpriteA' SyncFreeze $ mkAnimation fillDur $ \t -> withGroupOpacity t $ mkGroup [ctx node] where fillDur = 0.3 _oBalloon :: SVG -> Animation _oBalloon = animate . balloon -- FIXME: Also transform attributes: 'opacity', 'scale', 'scaleOrigin'. -- | Morph source object into target object over a set duration. oTransform :: Object s a -> Object s b -> Duration -> Scene s () oTransform src dst d = do srcSvg <- oRead src oSVG srcCtx <- oRead src oContext srcEase <- oRead src oEasing srcLoc <- oRead src oTranslate oModify src $ oShown .~ False dstSvg <- oRead dst oSVG dstCtx <- oRead dst oContext dstLoc <- oRead dst oTranslate m <- newObject $ Morph 0 (srcCtx srcSvg) (dstCtx dstSvg) oModifyS m $ do oShown .= True oEasing .= srcEase oTranslate .= srcLoc fork $ oTween m d $ \t -> oTranslate %~ lerp t dstLoc oTweenV m d $ \t -> morphDelta .~ t oModify m $ oShown .~ False oModify dst $ oShown .~ True ------------------------------------------------------------------------------- -- Built-in objects -- | Basic object mapping to \ in SVG. newtype Circle = Circle {_circleRadius :: Double} -- | Circle radius in local units. circleRadius :: Lens' Circle Double circleRadius = iso _circleRadius Circle instance Renderable Circle where toSVG (Circle r) = mkCircle r -- | Basic object mapping to \ in SVG. data Rectangle = Rectangle {_rectWidth :: Double, _rectHeight :: Double} -- | Rectangle width in local units. rectWidth :: Lens' Rectangle Double rectWidth = lens _rectWidth $ \obj val -> obj {_rectWidth = val} -- | Rectangle height in local units. rectHeight :: Lens' Rectangle Double rectHeight = lens _rectHeight $ \obj val -> obj {_rectHeight = val} instance Renderable Rectangle where toSVG (Rectangle w h) = mkRect w h -- | Object representing an interpolation between SVG nodes. data Morph = Morph {_morphDelta :: Double, _morphSrc :: SVG, _morphDst :: SVG} -- | Control variable for the interpolation. A value of 0 gives the -- source SVG and 1 gives the target svg. morphDelta :: Lens' Morph Double morphDelta = lens _morphDelta $ \obj val -> obj {_morphDelta = val} -- | Source shape. morphSrc :: Lens' Morph SVG morphSrc = lens _morphSrc $ \obj val -> obj {_morphSrc = val} -- | Target shape. morphDst :: Lens' Morph SVG morphDst = lens _morphDst $ \obj val -> obj {_morphDst = val} instance Renderable Morph where toSVG (Morph t src dst) = morph linear src dst t -- | Cameras can take control of objects and manipulate them -- with convenient pan and zoom operations. data Camera = Camera instance Renderable Camera where toSVG Camera = None -- | Connect an object to a camera such that -- camera settings (position, zoom, and rotation) is -- applied to the object. -- -- Example -- -- @ -- do cam \<- 'newObject' 'Camera' -- circ \<- 'newObject' $ 'Circle' 2 -- 'oModifyS' circ $ -- 'oContext' .= 'withFillOpacity' 1 . 'withFillColor' "blue" -- 'oShow' circ -- 'cameraAttach' cam circ -- 'cameraZoom' cam 1 2 -- 'cameraZoom' cam 1 1 -- @ -- -- <> cameraAttach :: Object s Camera -> Object s a -> Scene s () cameraAttach cam obj = spriteModify (objectSprite obj) $ do camData <- unVar (objectData cam) return $ \(svg, zindex) -> let V2 x y = camData ^. oTranslate ctx = translate (- x) (- y) . uncurryV2 translate (camData ^. oScaleOrigin) . scale (camData ^. oScale) . uncurryV2 translate (negate $ camData ^. oScaleOrigin) in (ctx svg, zindex) -- | -- -- Example -- -- @ -- do cam \<- 'newObject' 'Camera' -- circ \<- 'newObject' $ 'Circle' 2; 'oShow' circ -- 'oModify' circ $ 'oTranslate' .~ (-3,0) -- box \<- 'newObject' $ 'Rectangle' 4 4; 'oShow' box -- 'oModify' box $ 'oTranslate' .~ (3,0) -- 'cameraAttach' cam circ -- 'cameraAttach' cam box -- 'cameraFocus' cam (-3,0) -- 'cameraZoom' cam 2 2 -- Zoom in -- 'cameraZoom' cam 2 1 -- Zoom out -- 'cameraFocus' cam (3,0) -- 'cameraZoom' cam 2 2 -- Zoom in -- 'cameraZoom' cam 2 1 -- Zoom out -- @ -- -- <> cameraFocus :: Object s Camera -> V2 Double -> Scene s () cameraFocus cam new = do origin <- oRead cam oScaleOrigin t <- oRead cam oTranslate s <- oRead cam oScale let newLocation = new - ((new - origin) ^* s + origin - t) oModifyS cam $ do oTranslate .= newLocation oScaleOrigin .= new -- | Instantaneously set camera zoom level. cameraSetZoom :: Object s Camera -> Double -> Scene s () cameraSetZoom cam s = oModifyS cam $ oScale .= s -- | Change camera zoom level over a set duration. cameraZoom :: Object s Camera -> Duration -> Double -> Scene s () cameraZoom cam d s = oTweenS cam d $ \t -> oScale %= \v -> fromToS v s t -- | Instantaneously set camera location. cameraSetPan :: Object s Camera -> V2 Double -> Scene s () cameraSetPan cam location = oModifyS cam $ oTranslate .= location -- | Change camera location over a set duration. cameraPan :: Object s Camera -> Duration -> V2 Double -> Scene s () cameraPan cam d pos = oTweenS cam d $ \t -> oTranslate %= lerp t pos