The (Finite) State of

Reactive Animations

@davidkpiano · RxJS Live 2019

📞

🔙

🙏🤐

🅰️⚓️

🅰️🏋️‍♀️

🔭

🅰️

⚽️🎾🎱

Callbacks

Promises

Async-Await

Observables

Time is the most neglected variable.

A "reactive animation" is one involving
discrete changes, due to events.

Discrete

Continuous

By allowing programmers to
express the "what" of an interactive
animation, one can hope to then

automate the "how" of its presentation.

  • RxJS Core Team Member
  • Work at Google
  • Google Developer Expert
  • Angular Developer
  • Made a PR once (rejected)
  • Work at Microsoft
  • Googles things to pretend I'm an expert developer
  • React Developer

@davidkpiano 🎹

sorry conference wifi

loading...

I warned you there would be lots of demos

const animFrame$ =
  interval(0, animationFrameScheduler);

const smoothMove$ = animFrame$.pipe(
  withLatestFrom(move$, (frame, move) => move)
);

Animation Frames

In RxJS

.pipe(withLatestFrom(ball$, 
  (frame, ball) => ball))
animFrame$
const lerp = (current, next) => {
  const dx = next.x - current.x;
  const dy = next.y - current.y;
  const ratio = 0.1;

  return {
    x: current.x + dx * ratio;
    y: current.y + dy * ratio;
  };
};

200

300

deltaX = 300 - 200 = 100

100 * 0.1 = 10

200 + 10 = 210

210

300

100

Linear Interpolation (LERP)

Linear Interpolation (LERP)

1.0

0.5

0.2

ball$.pipe(
  scan((a, b) => lerp(a, b)))

1

1

1

2

2

2

3

3

1

1

1

1.5

1.75

2

2.5

2.75

const lerp = // ...

const animFrame$ = // ...

const smoothMove$ = animFrame$.pipe(
  withLatestFrom(move$, (frame, move) => move)
  scan(lerp));

smoothMove$.subscribe(values => {
  // render values
});

oh my 🐐

STREAM

VALUES

CUSTOM PROPERTIES

REACTIVE STYLES

Everything is a stream

...is not enough

Animations are transitions

between states

due to events

Finite state machines

and statecharts

Finite state machines

  • have one initial state

  • a finite number of states

  • a finite number of events

  • a mapping of state transitions 
    triggered by events

  • a finite number of final states

Idle

Pending

Rejected

Fulfilled

Fetch

Resolve

reject

Fulfilled

Rejected

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

SEARCH

SEARCH

🚫

SEARCH?

Define transitions between
states & events

function searchReducer(state = 'idle', event) {
  switch (state) {
    case 'idle':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    case 'searching':
      switch (event.type) {
        case 'RESOLVE':
          return 'success';
        case 'REJECT':
          return 'failure';
        default:
          return state;
      }
    case 'success':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    case 'failure':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    default:
      return state;
  }
}

FSMs with switch/case

const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
};

Define transitions between
states & events

FSMs with
object mapping

const transition = (state, event) => {
  return machine
    .states[state] // current state
    .on[event]     // next state
    || state;      // or same state
}
merge(...)
  scan(reducer, initial)

initial

next

pipe(

)
import { fromEvent, fromEventPattern, merge } from 'rxjs';
import { scan } from 'rxjs/operators';

const pan$ = fromEventPattern(/* ... */);
const esc$ = fromEvent(/* ... */);
const other$ = fromEventPattern(/* ... */);

const event$ = merge(pan$, esc$, other$);

const initialState = {
  // ...
}

const state$ = event$.pipe(
  scan((state, event) => {
    // state machine logic here!
  }, initialState)
);

state$.subscribe(state => {
  // render state
});
npm install xstate


const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
};
import { Machine } from 'xstate';

const machine = Machine({
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
});
import { Machine } from 'xstate';

const machine = Machine({
  // ...
});

const searchingState = machine
  .transition('idle', 'searching');

searchingState.value;
// => 'searching'
import { Machine, interpret } from 'xstate';

const machine = Machine({
  // ...
});

const service = interpret(machine)
  .onTransition(state => console.log(state))
  .start();

service.send('SEARCH');
// State {
//   value: 'searching',
//   ...
// }
const weekMachine = Machine({
  // ...
  states: {
    dragging: {
      invoke: [
        { src: mouseMove$ },
        { src: mouseUp$ }
      ],
      on: {
        mousemove: {
          actions: drag
        },
        mouseup: [
          { target: "selected", cond: isNotTrashElement },
          { target: "disposed", actions: showDaysRemoved }
        ]
      }
    },
    selected: {/* ... */},
    disposed: {/* ... */}
  }
});

dragging

selected

disposed

entry: subscribe mouseMove$

entry: subscribe mouseUp$

exit: unsubscribe mouseMove$

exit: unsubscribe mouseUp$

mouseup

mouseup

MOUSEMOVE

mouseMove$

mouseUp$

dragging

selected

disposed

grabbing

mouseDown$

import { interpret } from 'xstate';

const weekService = interpret(weekMachine)
  .start(); // start interpreting the machine

// services are observables!
weekService.subscribe(state => {
  console.log(state);
});

...etc.

Think in states,
not just events.

Advantages

of using statecharts

  • Visualized modeling
  • Precise di​agrams
  • Automatic code generation
  • Comprehensive test coverage
  • Accommodation of late-breaking requirements changes

The future of state

is nothing new.

The world of statecharts

Spectrum community

Make your code do more.

Thank you RxJS Live!

@davidkpiano · RxJS Live 2019

The Finite State of Reactive Animations

By David Khourshid

The Finite State of Reactive Animations

  • 4,694