Sometimes the show and hide animations provided by the decorator @layout.animate() does not suffice, and you want to animate in between multiple different states of a component, each with their own animation curves. For this granularity, we developed a mechanism called Flow.

The core concept of Flow is that renderables can have multiple states, each of which contain a collection of layout properties. When the renderable changes from one state to another, their layout properties are tweened into each other, creating the effect of seamless animation.

Let's start out with something easy: A button doing nothing.

import Surface                  from 'famous/core/Surface.js';
import {View}                   from 'arva-js/core/View.js';
import {layout, event, flow}    from 'arva-js/layout/Decorators.js';

import Easing                   from 'famous/transitions/Easing.js';

export class HomeView extends View {

    @flow.defaultState('initial', {}, layout.translate(0, 50, 0), layout.stick.bottom(), layout.size(100, 100))
    button = new Surface({
        properties: {
            backgroundColor: 'antiquewhite',
            borderRadius: '30%'
        }
    });
}

The key difference compared to declaring properties normally is that we're now putting the layout decorators at the end arguments of @flow.defaultState(stateName, options, ...layoutDecorators).

So far, this View doesn't give us any candy at all, it just looks like more complicated way of defining the layout and it won't animate. But bare with me for a second and look at what we're doing now:

    @flow.defaultState('initial', {}, layout.translate(0, 50, 0), layout.stick.bottom(), layout.size(100, 100))
    @event.on('mouseover', function () { this.setRenderableFlowState('button','big'); })
    @event.on('click', function () { this.setRenderableFlowState('button','initial'); })
    @flow.stateStep('big', {transition: {duration: 200}}, layout.stick.center())
    @flow.stateStep('big', {
        transition: {
            curve: Easing.inCubic,
            duration: 500
        }
    },
        layout.size((width, height) => width * 2, (width, height) => height * 2),
        layout.rotateFrom(0, 0, 2*Math.PI)
    )
    button = new Surface({
        properties: {
            backgroundColor: 'antiquewhite',
            borderRadius: '30%'
        }
    });

So what's going on here?

  • A state consists of one or multiple steps
    • A step is define by using the decorator @flow.stateStep which defines one or more things that is being executed in the step.
  • The state is executed by calling this.setRenderableFlowState(renderableName, stateName) which in the above example is done on click and mouseout. Try it out!
  • Decorators applied to each of the states are additions to the previous state. That is why we added layout.scale(1, 1, 1) and layout.skew(0, 0, 0) to the default state in order to reset previous changes.
  • Useful decorator functions for defining flow are layout.rotateFrom and layout.translateFrom which changes the renderable rotation/translation starting from the current state, which is why the Hello World text doesn't have to rotate back when going to the initial state.

View states

The state of a view consists of multiple states of its renderables. Let's create some text that shows up when the button finished animating to it's bigger state.

    @flow.defaultState('hidden', {}, 
        layout.opacity(0), 
        layout.size(~300, ~30), 
        layout.stick.center())
    @flow.stateStep('shown', {}, 
        layout.opacity(1))
    text = new Surface({content: "Welcome!"})

This text should now show after the button is covering the entire screen size. We'll do that by defining view states:

@flow.viewStates({
    enabled: [{button: 'big'}, {text: 'shown'}],
    passive: [{button: 'initial', text: 'hidden'}]
})
export class HomeView extends View {
...

Instead of calling this.setRenderableFlowState in the events of the button, we now call this.setViewFlowState, like this:

    @flow.defaultState('initial', {},
        layout.translate(0, 50, 0),
        layout.stick.bottom(),
        layout.size(100, 100))
    @event.on('mouseover', function () {
        this.setViewFlowState('enabled');
    })
    @event.on('click', function () {
        this.setViewFlowState('passive');
    })
    @flow.stateStep('big', {transition: {duration: 200}}, layout.stick.center())
    @flow.stateStep('big', {
        transition: {
            curve: Easing.inCubic,
            duration: 500
        }
    },
        layout.size((width, height) => width * 2, (width, height) => height * 2),
        layout.rotateFrom(0, 0, 2*Math.PI)
    )
    button = new Surface({
        properties: {
            backgroundColor: 'antiquewhite',
            borderRadius: '30%'
        }
    });

This now grows the button and afterwards shows the text. When the background is clicked, the text fades back to opacity zero and the button shrinks back.

Beyond states

Flow doesn't have to be limited to states. We can use this.decorateRenderable(...) at runtime to animate to any other state. Let's add more chaos.

    @flow.defaultState('hidden', {}, 
        layout.opacity(0), 
        layout.size(~300, ~30), 
        layout.stick.center())
    @flow.stateStep('shown', {}, 
        layout.opacity(1))
    @event.on('click', function () {
        this.decorateRenderable('text', 
            layout.translate(0, 300 * Math.random() - 150, 0))
    })
    text = new Surface({content: "Welcome!"})

The click will move the content to random position along the y axis. Note that the new position the message appears in is the same made by the last translate due to the additive nature of the decorator (state is applied from the current specification).

We use states in Arva for declaring different declarations along with the renderable itself, while this.decorateRenderable covers the usecases when the there a lot of unpredictable states (like in this example, randomized).

📘

Default transitions

The default transition is based on a spring simulation executed by the Famous physics engine. There are three cases when this could happen:

  • When there is no transition specified in the @flow decorator.
  • When this.decorateRenderable is used.
  • When one animation is interrupted by another one.

All together now

The entire code of our example looks like this:

import Surface                  from 'famous/core/Surface.js';
import {View}                   from 'arva-js/core/View.js';
import {layout, event, flow}    from 'arva-js/layout/Decorators.js';

import Easing                   from 'famous/transitions/Easing.js';

@flow.viewStates({
    enabled: [{button: 'big'}, {text: 'shown'}],
    passive: [{button: 'initial', text: 'hidden'}]
})
export class HomeView extends View {

    @flow.defaultState('initial', {},
        layout.translate(0, 50, 0),
        layout.stick.bottom(),
        layout.size(100, 100))
    @event.on('mouseover', function () {
        this.setViewFlowState('enabled');
    })
    @event.on('click', function () {
        this.setViewFlowState('passive');
    })
    @flow.stateStep('big', {transition: {duration: 200}}, layout.stick.center())
    @flow.stateStep('big', {
        transition: {
            curve: Easing.inCubic,
            duration: 500
        }
    },
        layout.size((width, height) => width * 2, (width, height) => height * 2),
        layout.rotateFrom(0, 0, 2*Math.PI)
    )
    button = new Surface({
        properties: {
            backgroundColor: 'antiquewhite',
            borderRadius: '30%'
        }
    });

    @flow.defaultState('hidden', {},
        layout.opacity(0),
        layout.size(~300, ~30),
        layout.stick.center())
    @flow.stateStep('shown', {},
        layout.opacity(1))
    @event.on('click', function () {
        this.decorateRenderable('text',
            layout.translate(0, 300 * Math.random() - 150, 0))
    })
    text = new Surface({content: "Welcome!"})
    
}