Skip to content

Commit bbad0d8

Browse files
committed
[css-animationworklet] Update README
- Drop parallax example since it does not showcase Animation Worklet power - Add new Swipe-to-Dismiss example and link to input event for worker/worklet proposal - Clean up related feature section and links
1 parent 165b94f commit bbad0d8

File tree

2 files changed

+147
-68
lines changed

2 files changed

+147
-68
lines changed

css-animationworklet/README.md

Lines changed: 147 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,14 @@ function createSpring(springConstant, ratio) {
224224
}
225225
```
226226

227+
Note that ideally once sping simulation is finished, the worklet animation would also dispatch
228+
the `finish` event. Adding the necessary mechanism to enable this is tracked
229+
[here](https://github.com/w3c/css-houdini-drafts/issues/808).
230+
227231
## Twitter Header
228232

233+
Note: This assumes experimental [ScrollTimeline][scroll-timeline] feature.
234+
229235
An example of twitter profile header effect where two elements (avatar, and header) are updated in
230236
sync with scroll offset.
231237

@@ -281,72 +287,137 @@ registerAnimator('twitter-header', class TwitterHeader extends StatelessAnimator
281287
   effect.children[1].localTime = this.timing_(this.clamp(scroll, 0, 1));
282288
}
283289
});
284-
285290
```
286291

287-
## Parallax
292+
## Swipe-to-Dissmiss
288293

289-
```html
290-
<style>
291-
.parallax {
292-
position: fixed;
293-
top: 0;
294-
left: 0;
295-
opacity: 0.5;
296-
}
297-
</style>
298-
<div id='scrollingContainer'>
299-
<div id="slow" class="parallax"></div>
300-
<div id="fast" class="parallax"></div>
301-
</div>
302-
<script>
303-
await CSS.animationWorklet.addModule('parallax-animator.js');
294+
Another usecase for Animation Worklet is to enable interactive input-driven animation effects that
295+
are driven both by input events and time.
304296

305-
const parallaxSlowEl = document.getElementById('slow');
306-
const parallaxFastEl = document.getElementById('fast');
307-
const scrollingContainer = = document.getElementById('scrollingContainer');
297+
To enable this we need a way to receive pointer events in worklet (e.g. via [CSS custom
298+
variables](https://github.com/w3c/css-houdini-drafts/issues/869) or [other
299+
mechanisms][input-for-worker]) and
300+
also allow [playback controls](https://github.com/w3c/css-houdini-drafts/issues/808) inside
301+
worklets. Both of these are natural planned additions to Animation Worklet.
308302

309-
const scrollTimeline = new ScrollTimeline({
310-
scrollSource: scrollingContainer,
311-
orientation: 'block',
312-
timeRange: 1000
313-
});
314-
const scrollRange = scrollingContainerEl.scrollHeight - scrollingContainerEl.clientHeight;
315303

316-
const slowParallax = new WorkletAnimation(
317-
'parallax',
318-
new KeyframeEffect(parallaxSlowEl, [{'transform': 'translateY(0)'}, {'transform': 'translateY(' + -scrollRange + 'px)'}], scrollRange),
319-
scrollTimeline,
320-
{rate : 0.4}
321-
);
322-
slowParallax.play();
304+
Consider a simple swipe-to-dismiss effect which follows the user swipe gesture and when finger lifts
305+
then continues to completion (either dismissed or returned to origin) with a curve that matches the
306+
swipe gesture's velocity. (See this [example](https://twitter.com/kzzzf/status/917444054887124992))
323307

324-
const fastParallax = new WorkletAnimation(
325-
'parallax',
326-
new KeyframeEffect(parallaxFastEl, [{'transform': 'translateY(0)'}, {'transform': 'translateY(' + -scrollRange + 'px)'}], scrollRange),
327-
scrollTimeline,
328-
{rate : 0.8}
329-
);
330-
fastParallax.play();
331-
</script>
308+
With Animation Worklet, this can be modeled as a stateful animator which consumes both time and
309+
pointer events and have the following state machines:
332310

333-
```
311+
![SwipeToCompletionAnimation](img/swipe-to-dismiss-state.png)
334312

335-
parallax-animator.js:
336313

337-
```js
338-
// Inside AnimationWorkletGlobalScope
339-
registerAnimator('parallax', class Parallax extends StatelessAnimator{
340-
constructor(options) {
341-
this.rate_ = options.rate;
314+
Here are the three main states:
315+
316+
1. Animation is idle, where it is `paused` so that it is not actively ticking
317+
2. As soon as the user touches down, the animation moves the target to follow the user touchpoint
318+
while staying `paused` (optionally calculate the movement velocity, and overall delta).
319+
3. As soon as the user lift their finger the animation will the switch to 'playing' so that it is
320+
ticked by time until it reaches its finished state. The final state may be decided on overall
321+
delta and velocity and the animation curve adapts to the movement velocity.
322+
323+
Note that while in (3), if the user touches down we go back to (2) which ensures responsiveness to
324+
user touch input.
325+
326+
To make this more concrete, here is how this may be implemented (assuming strawman proposed APIs for
327+
playback controls and also receiving pointer events). Note that all the state machine transitions and
328+
various state data (velocity, phase) and internal to the animator. Main thread only needs to provide
329+
appropriate keyframes that can used to translate the element on the viewport as appropriate (e.g.,
330+
`Keyframes(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']})`).
331+
332+
333+
```javascript
334+
registerAnimator('swipe-to-dismiss', class SwipeAnimator extends StatefulAnimator {
335+
constructor(options, state = {velocity: 0, phase: 'idle'}) {
336+
this.velocity = state.velocity;
337+
this.phase = state.phase;
338+
339+
if (phase == 'idle') {
340+
// Pause until we receive pointer events.
341+
this.pause();
342+
}
343+
344+
// Assumes we have an API to receive pointer events for our target.
345+
this.addEventListener("eventtargetadded", (event) => {
346+
for (type of ["pointerdown", "pointermove", "pointerup"]) {
347+
event.target.addEventListener(type,onPointerEvent );
348+
}
349+
});
350+
}
351+
352+
onpointerevent(event) {
353+
if (event.type == "pointerdown" || event.type == "pointermove") {
354+
this.phase = "follow_pointer";
355+
} else {
356+
this.phase = "animate_to_completion";
357+
// Also decide what is the completion phase (e.g., hide or show)
358+
}
359+
360+
this.pointer_position = event.screenX;
361+
362+
// Allow the animation to play for *one* frame to react to the pointer event.
363+
this.play();
342364
}
343365

344366
animate(currentTime, effect) {
345-
effect.localTime = currentTime * this.rate_;
367+
if (this.phase == "follow_pointer") {
368+
effect.localTime = position_curve(this.pointer_position);
369+
update_velocity(currentTime, this.pointer_position);
370+
// Pause, no need to produce frames until next pointer event.
371+
this.pause();
372+
} else if (this.phase = "animate_to_completion") {
373+
effect.localTime = time_curve(currentTime, velocity);
374+
375+
if (effect.localTime == 0 || effect.localTime == effect.duration) {
376+
// The animation is complete. Pause and become idle until next user interaction.
377+
this.phase = "idle";
378+
this.pause();
379+
} else {
380+
// Continue producing frames based on time until we complete or the user interacts again.
381+
this.play();
382+
}
383+
}
384+
385+
}
386+
387+
position_curve(x) {
388+
// map finger position to local time so we follow user's touch.
389+
}
390+
391+
time_curve(time, velocity) {
392+
// Map current time delta and given movement velocity to appropriate local time so that over
393+
// time we animate to a final position.
394+
}
395+
396+
update_velocity(time, x) {
397+
this.velocity = (x - last_x) / (time - last_time);
398+
this.last_time = time;
399+
this.last_x = x;
400+
}
401+
402+
state() {
403+
return {
404+
phase: this.phase,
405+
velocity: this.velocity
406+
}
346407
}
347408
});
348409
```
349410

411+
```javascript
412+
413+
await CSS.animationWorklet.addModule('swipe-to-dismiss-animator.js');
414+
const target = document.getElementById('target');
415+
const s2d = new WorkletAnimation(
416+
'swipe-to-dismiss',
417+
new KeyframeEffect(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']}));
418+
s2d.play();
419+
```
420+
350421

351422
# Why Extend Animation?
352423

@@ -465,7 +536,7 @@ registerAnimator('animation-with-local-state', class FoorAnimator extends Statef
465536
return {
466537
this.velocity,
467538
this.acceleration
468-
};
539+
}
469540
}
470541

471542
curve(velocity, accerlation, t) {
@@ -476,27 +547,34 @@ registerAnimator('animation-with-local-state', class FoorAnimator extends Statef
476547

477548
# Related Concepts
478549

479-
The following concepts are not part of Animation Worklet specification but animation worklet is
480-
designed to take advantage of them to enable a richer set of usecases.
550+
The following concepts are not part of Animation Worklet specification but Animation Worklet is
551+
designed to take advantage of them to enable a richer set of usecases. These are still in early
552+
stages of the standardization process so their API may change over time.
481553

482554
## ScrollTimeline
483-
[ScrollTimeline](https://wicg.github.io/scroll-animations/#scrolltimeline) is a concept introduced in
555+
[ScrollTimeline][scroll-timeline] is a concept introduced in
484556
scroll-linked animation proposal. It defines an animation timeline whose time value depends on
485557
scroll position of a scroll container. `ScrollTimeline` can be used an an input timeline for
486558
worklet animations and it is the intended mechanisms to give read access to scroll position.
487559

560+
We can later add additional properties to this timeline (e.g., scroll phase (active, inertial, overscroll),
561+
velocity, direction) that can further be used by Animation Worklet.
562+
488563
## GroupEffect
489-
[GroupEffect](https://w3c.github.io/web-animations/level-2/#the-animationgroup-interfaces) is a
490-
concept introduced in Web Animation Level 2 specification. It provides a way to group multiple
491-
effects in a tree structure. `GroupEffect` can be used as the output for worklet animations. It
492-
makes it possible for worklet animation to drive effects spanning multiple elements.
493-
494-
**TODO**: At the moment, `GroupEffect` only supports just two different scheduling models (i.e.,
495-
parallel, sequence). These models governs how the group effect time is translated to its children
496-
effect times by modifying the child effect start time. Animation Worklet allows a much more
497-
flexible scheduling model by making it possible to to set children effect's local time directly. In
498-
other words we allow arbitrary start time for child effects. This is something that needs to be
499-
added to level 2 spec.
564+
565+
[GroupEffect][group-effect] is a concept introduced in Web Animation Level 2 specification. It
566+
provides a way to group multiple effects in a tree structure. `GroupEffect` can be used as the
567+
output for Worklet Animations making it possible for it to drive complext effects spanning multiple
568+
elements. Also with some minor [proposed changes](group-effect-changes) to Group Effect timing
569+
model, Animation Worklet can enable creation of new custom sequencing models (e.g., with conditions
570+
and state).
571+
572+
## Event Dispatching to Worker and Worklets
573+
[Event Dispatching to Worker/Worklets][input-for-worker] is a proposal in WICG which allows workers
574+
and worklets to passively receive DOM events and in particular Pointer Events. This can be
575+
benefitial to Animation Worklet as it provides an ergonomic and low latency way for Animation
576+
Worklet to receive pointer events thus enabling it to implement input driven animations more
577+
effectively.
500578

501579
# WEBIDL
502580

@@ -508,6 +586,7 @@ the animation worklet scope.
508586
- A sequence of timelines, the first one of which is considered primary timeline and passed to
509587
`Animation` constructor.
510588

589+
511590
```webidl
512591
513592
[Constructor (DOMString animatorName,
@@ -519,10 +598,6 @@ interface WorkletAnimation : Animation {
519598
}
520599
```
521600

522-
**TODO**: At the moment `GroupEffect` constructor requires a timing but this seems unnecessary for
523-
`WorkletAnimation` where it should be possible to directly control individual child effect local
524-
times. We need to bring this up with web-animation spec.
525-
526601
`AnimationEffectReadOnly` gets a writable `localTime` attribute which may be used to drive the
527602
effect from the worklet global scope.
528603

@@ -545,3 +620,7 @@ the most recent version.
545620
[WA]: https://drafts.csswg.org/web-animations/
546621
[animation]: https://drafts.csswg.org/web-animations/#animations
547622
[worklet]: https://drafts.css-houdini.org/worklets/#worklet-section
623+
[input-for-worker]: https://discourse.wicg.io/t/proposal-exposing-input-events-to-worker-threads/3479
624+
[group-effect]: https://w3c.github.io/web-animations/level-2/#the-animationgroup-interfaces
625+
[group-effect-changes]: https://github.com/yi-gu/group_effects
626+
[scroll-timeline]: https://wicg.github.io/scroll-animations/#scrolltimeline
Loading

0 commit comments

Comments
 (0)