Write cool stateful animations with js-coroutines
Tags: javascript, webdev, react
Published: 2020-05-31 18:47:22

## TL;DR

* There is a way of writing animations that you've probably never heard of
* It makes writing animation code much simpler because it's imperative: you can use for-next loops and while statements
* My [js-coroutines](https://github.com/miketalbot/js-coroutines) library let's you write stateful *coroutines* for things like reactive animations
* You write simple stateful `generator` functions and then fire and forget
* Below is a React example of a simple reactive magnify animation

## Magnify Demo

{% codesandbox zeq33 %}

## Magnify

The magnify effect increases the size of an item as the mouse approaches it, then animates its exit state as a flip should the mouse enter and then leave it.  This is a useful example of stateful *coroutines*.

I've implemented it as a React wrapper component that can perform the effect on its children.

```jsx
export function MagnifyBox({
    children,
    from = 1,
    to = 1.8,
    flipFrames = 60,
    radius = 15,
    ...props
}) {
    const ref = useRef()
    const classes = useStyles()
    useEffect(() => {
        const promise = magnify(ref.current, from, to, radius, flipFrames)
        return promise.terminate
    })
    return (
        <Box ref={ref} className={classes.magnify} {...props}>
            {children}
        </Box>
    )
}
```

Here we create a simple Material UI Box wrapper that creates a coroutine in it's useEffect and calls the exit function of the coroutine should it unmount.

### The coroutine

The `magnify` call creates a coroutine to perform the animation:

```jsx
export function magnify(
    element,
    from = 0.9,
    to = 1.5,
    radius = 5,
    flipFrames = 60
) {
    if (!element) return
    const pos = rect()
    const zIndex = element.style.zIndex || 0
    const initialTransform = element.style.transform || ""
    const SCALING_FACTOR = pos.width + pos.height * 2
    //Get js-coroutines to run our function in high priority
    return update(run)
    ...
```

The first part of the function grabs some useful stuff from the element to be animated and uses [js-coroutines](https://github.com/miketalbot/js-coroutines)  to start a high priority update animation.

Then we have 2 animation states, the first one is about the mouse approaching the item, the second about flipping.  In the main animation we resize the item based on mouse position and then check if we are moving from inside to outside, which should trigger the flip.

```jsx
    //Standard animation
    function* run() {
        let inside = false
        while (true) {
            //Resize based on mouse position
            const [, middleX] = resize()
            const outside = Math.abs(middleX - x) > pos.width
            if (!outside) {
                inside = true
            } else {
                if (inside) {
                    inside = false
                    //Use the flip animation until complete
                    yield* flip(middleX > x ? 1 : -1)
                }
            }
            yield
        }
    }

```

`resize` performs cursor distance resizing:

```jsx
    function resize() {
        const pos = rect()
        let middleX = pos.width / 2 + pos.x
        let middleY = pos.height / 2 + pos.y
        let d = Math.sqrt((x - middleX) ** 2 + (y - middleY) ** 2)
        const value = lerp(to, from, clamp((d - radius) / SCALING_FACTOR))
        element.style.transform = `scale(${value}) ${initialTransform}`
        element.style.zIndex =
            zIndex + ((((value - from) / (to - from)) * 1000) | 0)
        return [d, middleX, middleY]
    }

    function clamp(t) {
         return Math.max(0, Math.min(1, t))
    } 

    function lerp(a, b, t) {
        return (b - a) * t + a
    }
```

Then when it's time to flip, we just do a `for-next` loop, which is the joy of using a stateful *generator function* when writing imperative animations that execute over multiple frames:

```jsx
    function* flip(direction = 1) {
        for (let angle = 0; angle < 360; angle += 360 / flipFrames) {
            //Still perform the resize
            resize()
            //Make the item "grey" on the back  
            if (angle > 90 && angle < 270) {
                element.style.filter = `grayscale(1)`
            } else {
                element.style.filter = ``
            }
            element.style.transform = `${
                element.style.transform
            } rotate3d(0,1,0,${angle * direction}deg)`
            //Wait until next frame
            yield
        }
    }
```

## Miscellany

Getting the mouse position is achieved by adding a global handler to the document:

```jsx
let x = 0
let y = 0

function trackMousePosition() {
    document.addEventListener("mousemove", storeMousePosition)
}

trackMousePosition()

function storeMousePosition(event) {
    x = event.pageX
    y = event.pageY
}

```

And then using the effect is a case of wrapping MagnifyBox around the content:

```jsx
           <Box mt={10} display="flex" flexWrap="wrap" justifyContent="center">
                {icons.map((Icon, index) => {
                    return (
                        <MagnifyBox key={index} mr={2} to={2.5} from={1}>
                            <IconButton
                                style={{
                                    color: "white",
                                    background: colors[index]
                                }}
                            >
                                <Icon />
                            </IconButton>
                        </MagnifyBox>
                    )
                })}
            </Box>
```

# Conclusion

Hopefully this example has shown how easy it is to write stateful animations using *generator functions* and js-coroutines!