WEB ANIMATIONS REDEFINED
Reactive Animations With
RxJS & CSS Variables
Florida
Australia
Austria
also Austria
WHAT ARE
A "reactive animation" is one involving discrete changes, due to events.
WHAT ARE
A "reactive animation" is one involving discrete changes, due to events.
By allowing programmers to express the "what" of an interactive animation, one can hope to then automate the "how" of its presentation.
WHAT ARE
CSSDevConf San Antonio
David Khourshid @davidkpiano
HAVE MANY THINGS IN COMMON
&
vs.
vs.
&
BUT THAT'S A GOOD THING
Powerful languages inhibit information reuse.
Use the least powerful language suitable for expressing information, constraints or programs on the WWW.
PRINCIPLE
GOOD PRACTICE
✅
box-sizing: soap-box;
STREAM
VALUES
CUSTOM PROPERTIES
REACTIVE STYLES
CUSTOM PROPERTIES
:root {
--my-color: white;
}
button {
background-color: var(--my-color, blue);
}
SPECIFICITY
INHERITANCE
CUSTOM PROPERTIES
button.special {
--my-color: red;
}
Wait for it...
element.style
.setProperty('--my-color', 'blue');
element.style
.getPropertyValue('--my-color');
// => 'blue'
element.style
.removeProperty('--my-color');
CUSTOM PROPERTIES
Isn't conference WIFI awesome?
BROWSER SUPPORT
Nobody cares
PREPROCESSOR SUPPORT
💅
Pretend this is a really cool demo
const docStyle = document.documentElement.style;
const ball = document.querySelector('.ball');
document.addEventListener('mousemove', (e) => {
ball.style.transform = `
translateX(${e.clientX}px)
translateY(${e.clientY}px)
`;
});
JAVASCRIPT
class Particle {
constructor() {
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this.
this.this.this.this.this.this
}
}
const docStyle = document.documentElement.style;
const ball = document.querySelector('.ball');
document.addEventListener('mousemove', (e) => {
ball.style.setProperty('--mouse-x', e.clientX);
ball.style.setProperty('--mouse-y', e.clientY);
});
.ball {
transform:
translateX(calc(var(--mouse-x) * 1px))
translateY(calc(var(--mouse-y) * 1px));
}
JAVASCRIPT
CSS
APP
STATE
MOUSE
TOUCH
AUDIO
ASYNC DATA
OTHER ANIMATIONS
CSS VARIABLES
[
]
ARRAYS
1s
3s
4s
6.5s
9.5s
STREAMS
Rx.Observable
.from([1, 2, 3, 4, 5]);
Rx.Observable
.fromEvent(document, 'mousemove');
Rx.Observable
.fromEventPattern((callback) =>
hammerFoo.on('pan', callback));
const ball = document.querySelector('#ball');
const hBall = new Hammer(ball);
const pan$ = Rx.Observable
.fromEventPattern((handler) =>
hBall.on('pan', handler));
pan$.subscribe((event) => {
// do anything you want
});
pan$.subscribe(
handleNext,
handleError,
handleComplete
);
Aggregate, All, Amb, and_, And, Any, apply, as_blocking, asObservable, AssertEqual, asyncAction, asyncFunc, Average, averageDouble, averageFloat, averageInteger, averageLong, blocking, Buffer, bufferWithCount, bufferWithTime, bufferWithTimeOrCount, byLine, cache, case, Cast, Catch, catchError, catchException, collect, collect (RxScala version of Filter), CombineLatest, combineLatestWith, Concat, concat_all, concatMap, concatMapObserver, concatMapTo, concatAll, concatWith, Connect, connect_forever, cons, Contains, controlled, Count, countLong, Create, cycle, Debounce, decode, DefaultIfEmpty, Defer, deferFuture, Delay, delaySubscription, delayWithSelector, Dematerialize, Distinct, distinctKey, distinctUntilChanged, distinctUntilKeyChanged, Do, doAction, doAfterTerminate, doOnCompleted, doOnEach, doOnError, doOnRequest, doOnSubscribe, doOnTerminate, doOnUnsubscribe, doseq, doWhile, drop, dropRight, dropUntil, dropWhile, ElementAt, ElementAtOrDefault, Empty, emptyObservable, empty?, encode, ensures, error, every, exclusive, exists, expand, failWith, Filter, filterNot, Finally, finallyAction, finallyDo, find, findIndex, First, FirstOrDefault, firstOrElse, FlatMap, flatMapFirst, flatMapIterable, flatMapIterableWith, flatMapLatest, flatMapObserver, flatMapWith, flatMapWithMaxConcurrent, flat_map_with_index, flatten, flattenDelayError, foldl, foldLeft, for, forall, ForEach, forEachFuture, forIn, forkJoin, From, fromAction, fromArray, FromAsyncPattern, fromCallable, fromCallback, FromEvent, FromEventPattern, fromFunc0, from_future, from_iterable, fromIterator, from_list, fromNodeCallback, fromPromise, fromRunnable, Generate, generateWithAbsoluteTime, generateWithRelativeTime, generator, GetEnumerator, getIterator, GroupBy, GroupByUntil, GroupJoin, head, headOption, headOrElse, if, ifThen, IgnoreElements, indexOf, interleave, interpose, Interval, into, isEmpty, items, Join, join (string), jortSort, jortSortUntil, Just, keep, keep-indexed, Last, lastOption, LastOrDefault, lastOrElse, Latest, latest (Rx.rb version of Switch), length, let, letBind, limit, LongCount, ManySelect, Map, map (RxClojure version of Zip), MapCat, mapCat (RxClojure version of Zip), map-indexed, mapTo, mapWithIndex, Materialize, Max, MaxBy, Merge, mergeAll, merge_concurrent, mergeDelayError, mergeObservable, mergeWith, Min, MinBy, MostRecent, Multicast, multicastWithSelector, nest, Never, Next, Next (BlockingObservable version), none, nonEmpty, nth, ObserveOn, ObserveOnDispatcher, observeSingleOn, of, of_array, ofArrayChanges, of_enumerable, of_enumerator, ofObjectChanges, OfType, ofWithScheduler, onBackpressureBlock, onBackpressureBuffer, onBackpressureDrop, OnErrorResumeNext, onErrorReturn, onExceptionResumeNext, orElse, pairs, pairwise, partition, partition-all, pausable, pausableBuffered, pluck, product, Publish, PublishLast, publish_synchronized, publishValue, raise_error, Range, Reduce, reductions, RefCount, Repeat, repeat_infinitely, repeatWhen, Replay, rescue_error, rest, Retry, retry_infinitely, retryWhen, Return, returnElement, returnValue, runAsync, Sample, Scan, scope, Select (alternate name of Map), select (alternate name of Filter), selectConcat, selectConcatObserver, SelectMany, selectManyObserver, select_switch, selectSwitch, selectSwitchFirst, selectWithMaxConcurrent, select_with_index, seq, SequenceEqual, sequence_eql?, SequenceEqualWith, Serialize, share, shareReplay, shareValue, Single, SingleOrDefault, singleOption, singleOrElse, size, Skip, SkipLast, skipLastWithTime, SkipUntil, skipUntilWithTime, SkipWhile, skipWhileWithIndex, skip_with_time, slice, sliding, slidingBuffer, some, sort, sort-by, sorted-list-by, split, split-with, Start, startAsync, startFuture, StartWith, startWithArray, stringConcat, stopAndWait, subscribe, SubscribeOn, SubscribeOnDispatcher, subscribeOnCompleted, subscribeOnError, subscribeOnNext, Sum, sumDouble, sumFloat, sumInteger, sumLong, Switch, switchCase, switchIfEmpty, switchLatest, switchMap, switchOnNext, Synchronize, Take, take_with_time, takeFirst, TakeLast, takeLastBuffer, takeLastBufferWithTime, takeLastWithTime, takeRight (see also: TakeLast), TakeUntil, takeUntilWithTime, TakeWhile, takeWhileWithIndex, tail, tap, tapOnCompleted, tapOnError, tapOnNext, Then, thenDo, Throttle, throttleFirst, throttleLast, throttleWithSelector, throttleWithTimeout, Throw, throwError, throwException, TimeInterval, Timeout, timeoutWithSelector, Timer, Timestamp, To, to_a, ToArray, ToAsync, toBlocking, toBuffer, to_dict, ToDictionary, ToEnumerable, ToEvent, ToEventPattern, ToFuture, to_h, toIndexedSeq, toIterable, toIterator, ToList, ToLookup, toMap, toMultiMap, ToObservable, toSet, toSortedList, toStream, ToTask, toTraversable, toVector, tumbling, tumblingBuffer, unsubscribeOn, Using, When, Where, while, whileDo, Window, windowWithCount, windowWithTime, windowWithTimeOrCount, windowed, withFilter, withLatestFrom, Zip, zipArray, zipWith, zipWithIndex
.filter(ball => ball.color === 'green')
.map(ball => makeSquare(ball))
.throttleTime(1000)
.scan((a, b) => a + b, 0)
1
2
3
5
6
8
1
1
1
2
1
2
If you can read this, the WIFI sucks
Just kidding, the WIFI isn't that bad
If you can read this, the wifi still sucks
Rx.Subject()
CSS Variables
mouseMove$
scroll$
tap$
swipe$
timer$
...etc.
(Observers)
An
Rx.Subject is both
an
observable and an
observer.
const mouse$ = Rx.Observable
.fromEvent(document, 'mousemove')
.map(({ clientX, clientY }) => ({
x: clientX,
y: clientY,
}));
const style$ = RxCSS({
mouse: mouse$,
});
style$.subscribe(...);
yarn whatever ¯\_(ツ)_/¯
:root {
--mouse-x: 0;
--mouse-y: 0;
}
.ball {
transform: translate(
calc(var(--mouse-x) * 1px))
calc(var(--mouse-y) * 1px))
);
}
0kb
Minified, GZIPped, Deleted
Oh my god
Please wifi just work
😭
wifi y u do dis
WHY ANIMATE WITH
RxCSS({
mouse: mouse$,
pan: pan$,
// ...
}).subscribe((values) => {
// do anything!
});
WHY ANIMATE WITH
WHY ANIMATE WITH
WHY ANIMATE WITH