2687 views
<center> # An Overview of Animation Programming **Declarative Animations in a Functional World** *Originally published 2018-10-31 by Nick Sweeting on [docs.Monadical.com](https://monadical.com/posts/functional-declarative-animations.html).* <img src="https://i.imgur.com/cbZA2jm.png" style="width: 80%; border-radius: 14px; box-shadow: 4px 4px 4px rgba(0,0,0,0.04);"> </center> <br/> [TOC] If you find yourself working with animations, chances are you're probably trying to do either: - minor content transitions (e.g. photo gallery transitions, hover effects, minor visuals); or - full-blown interactive dynamic animations (e.g. games, data visualization, or visual art) These two categories are fairly separate, and so naturally the solutions are quite unique, and using a system designed for one will often hinder development of the other. Today we're going to focus on the second, more advanced type of animations: *complex movement involving many objects over an extended period of time*. This summary of animation techniques showcases browser-based animation with Javascript and `rAF`, but the techniques are not specific to JS and can be used for animating in any language. You may have used libraries like GSAP, react-move, or jQuery to do animations in the past. Maybe you've heard terms like "tween" and "timeline" before, or maybe you're just using `setTimeout`. At one point or another though, all of us have likely experienced the frustration of working with an animation system that has mutable state scattered all over the place, causing spaghetti code that mixes user interaction and animation rendering. Today we're going to explore ways to design an animation system that allows us to keep all the animation state in one place, with a clean render loop that doesn't mix user interation and animation scheduling. ## What Is an Animation? ![](https://i.imgur.com/Q8cJeRk.png) At its heart, an animation is a transition from a start state to an end state, with some interpolated values in-between. With this simple defition in mind, it turns out we only need a few pieces of information to perfectly describe any animation: - A `start_state` and `end_state` to know where we start and where we end up - A `start_time` and `end_time` to know when the animation is active - A`curve_function` to describe the shape of the graph of the animation's movement (e.g. `linear`, `easeInOut`, etc) ![](https://i.imgur.com/hjsrE9P.png) In technical parlance, these values together form a "[Tween](https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_breakout_game_Phaser/Animations_and_tweens#Tweens)" (short for inbetween). Tweens are placed on a "[Timeline](https://greensock.com/timelinelite)" which controls the order of animations and the speed in which they play. This mental model of an animation is distorted when we have to cram it into imperative, stateful animation processes like so: ```jsx <div style="top: 100px" id="demo">abc</div> $('demo').animate({top: 200}, 2000) setTimeout(function() { $('demo').animate({top: 100}, 2000) }, 2000) ``` In this example, the starting states, end states, and durations are scattered around the code instead of being in a single data structure. Below, we'll explore why that becomes a problem as a codebase matures in complexity, and as testability and maintainability become more important. ## Patterns There are generally three overarching patterns you'll encounter when animating things with a render loop: - imperative stateful animation - pure functional animation - declarative animation Below are some quick pseudocode examples of each pattern, where we animate a ball moving by 3px across the screen. ### Imperative Animation Here we have some external mutable state that keeps track of how far we've progressed in the animation. This state needs to be kept track of and cleared/reset appropriately when animations start and end. ```jsx start_position = 15 step_size = 3 animate() { current_position = start_position // state that is updated on each run while true { current_position += step_size render(current_position) sleep(14ms) } } ``` `current_position` is mutable state that must be updated on each render loop for the animation to progress. ### Functional Animation We can improve on our previous example slightly by computing the animated state in a pure functional manner, instead of incrementing a mutable counter somewhere. Now we don't need to keep track of an external value, and we can deterministically calculate what the animation would look like at any point in time without having to play it forwards at normal speed. ```jsx start_position = 15 step_size = 3 ballPosition(step) { return start_position + step * step_size } animate() { for (step_number=0; true; step_number++) { render(ballPosition(step_number)) sleep(14ms) } } ``` The difference is subtle, but important: **in this example there is no mutable state**. The animation is always computed as a pure function of the start position and current timestamp. ### Declarative Animation As a system becomes more advanced, there is a natural tendency for procedures to transition from being inscribed as code, to expressed as data. For example, your typical CI/CD pipeline probably used to be a bash script, then at some point it transitioned to a YAML file with nice pretty descriptions for every step. Having as much of your complexity expressed as data aids in keeping layers of systems cleanly separated, and does wonders for testability. Suddenly you can pass around a datastructure that represents a sequence of events, and test it like data by making simple assertions about its content. LISP discovered the power of code as data long ago. I call this transition process **declarativizing**, because we declare what we want outcomes to be as data, instead of outlining steps to get there. Here's Javascript example of a declarative animation: ```jsx const rotate = { path: '/button/style/transform/rotate', start_time: 1540702920000, duration: 1000, start_state: 0, end_state: 180, unit: 'deg', curve: 'easeInOutQuad', } ``` This dictionary contains everything we need to know to rotate a button 180º on the screen. We get a start timestamp, a duration in milliseconds, the starting and ending positions, and the unit of the value we're animating (e.g. `deg`, `px`, `em`, `%`, etc.) . Notice that all these values are immutable, and this animation needs no external state in order to be rendered. Using only the current timestamp we can compute what this animation would look like at any given moment. <center> <img src="https://i.imgur.com/vnVzoro.png" width="60%"> </center> ## Implementing a Render Loop These buzzwords like "declarative", "functional", and "imperative" are just rough traits your code can have. A codebase isn't usually one type or another, rather all patterns exist in differing proportions in different codebases. The ideal solution probably uses a mixture of the three, with most complexity managed as data, a [functional core](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) to process it, and an imperative shell to control the animation timeline. Below, I'm going to demonstrate a Javascript render loop example that uses both functional and declarative styles to render an element growing in width over 2 seconds. *First we define the animation as data.* ```jsx // grow from 0% width to 100% const grow = { start_time: 1540702921000, duration: 2000, start_state: 0, end_state: 100, unit: '%', } ``` *Then we render this animation in a loop.* ```jsx const render = (timestamp) => { const width = compute_animated_state(grow, timestamp) document.getElementById('demo').style.width = width window.requestAnimationFrame(render) } ``` *We compute animated state based on the current timestamp and curve function.* ```jsx // given animation description and current timestamp const compute_animated_state = (anim, ts) => { const start_time = anim.start_time const duration = anim.duration const end_time = start_time + duration // if animating, calculate intermediate animated value if (ts >= start_time && ts <= end_time) { const delta_total = anim.end_state - anim.start_state const progress = (ts - start_time) / duration return start_state + (progress * delta_total) } // if not currently animating, return start or end state return ts > end_time ? end_state : start_state } ``` ![](https://i.imgur.com/9C0jbRi.png) ## Functional-Reactive Rendering With a way to store animations as data, and a pure-functional animation system, we can render everything using a reactive rendering engine like React, Inferno, or SnabbDOM. ```jsx const store = combineReducers({animations}) ... const mapStateToProps = ({animations}) => animations.state.ball const BallComponent = ({style}) => <div style={style}></div> const Ball = connect(mapStateToProps)(BallComponenet) ... const runloop = (timstamp) => { store.dispatch({type: 'TICK', timestamp: Date.now()}) window.requestAnimationFrame(runloop) } runloop() ReactDOM.render( <Provider store={store}> <Ball/> </Provider> ) ``` Virtual DOM diffing helps this process stay efficient, as only the animations that are running get computed, and only the elements that are actually changed by animations get re-rendered on each frame. Functional reactive rendering to DOM elements isn't the only way to do animation of course, you could also use something like ThreeJS or WebGL to animate your state to a canvas. ## Redux-time The principals of storing animation complexity as data and processing it functionally to produce rendered state sit at the core of animations library we created called `redux-time`. At OddSlingers, we use `redux-time` for rendering [real-time online poker](https://oddslingers.com) in the browser and staying fast even on older mobile devices. <center> <a href="https://github.com/Monadical-SAS/redux-time"> ![](https://github.com/Monadical-SAS/redux-time/blob/master/logo.png?raw=true) </a> We encorage you to check it out if you're working with complex animations in Javascript! https://github.com/Monadical-SAS/redux-time </center> ## tl; dr - we can represent complex animations as data instead of code - interpolate intermediate state using pure functions of those animations - we can render that state automatically with functional-reactive rendering ## Further Reading - [redux-time Docs](https://github.com/Monadical-SAS/redux-time) - [GSAP](https://greensock.com/gsap): incredibly robust, stable, well-supported Javascript animations library - [React Docs](https://facebook.github.io/react/docs/animation.html): on Animation - [react-move](https://github.com/tannerlinsley/react-move): react animations library - [React-f1](https://github.com/Jam3/react-f1): stateful animations library - [React-Transition-Group](https://github.com/reactjs/react-transition-group/tree/v1-stable) library to add component lifecycle CSS transitions - React-Transition-Group walkthrough article: [UI Animations with React — The Right Way](https://medium.com/@joethedave/achieving-ui-animations-with-react-the-right-way-562fa8a91935) - [react-animations](https://github.com/FormidableLabs/react-animations) CSS animations usable with inline-style libraries like StyledComponents - [react-animate](https://www.npmjs.com/package/react-animate) library for defining component transitions by extending the Component class - [React.rocks](https://react.rocks/tag/Animation): animation examples - [Animate.css](https://github.com/daneden/animate.css/blob/master/animate.css): repository of great css animations (usable with redux-time)