@davidkpiano · RxJS Live 2019
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.
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)
);
.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
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
Animations are transitions
between states
due to events
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;
}
}
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
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.
@davidkpiano · RxJS Live 2019