@@ -224,8 +224,14 @@ function createSpring(springConstant, ratio) {
224
224
}
225
225
```
226
226
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
+
227
231
## Twitter Header
228
232
233
+ Note: This assumes experimental [ ScrollTimeline] [ scroll-timeline ] feature.
234
+
229
235
An example of twitter profile header effect where two elements (avatar, and header) are updated in
230
236
sync with scroll offset.
231
237
@@ -281,72 +287,137 @@ registerAnimator('twitter-header', class TwitterHeader extends StatelessAnimator
281
287
effect .children [1 ].localTime = this .timing_ (this .clamp (scroll, 0 , 1 ));
282
288
}
283
289
});
284
-
285
290
```
286
291
287
- ## Parallax
292
+ ## Swipe-to-Dissmiss
288
293
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.
304
296
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.
308
302
309
- const scrollTimeline = new ScrollTimeline ({
310
- scrollSource: scrollingContainer,
311
- orientation: ' block' ,
312
- timeRange: 1000
313
- });
314
- const scrollRange = scrollingContainerEl .scrollHeight - scrollingContainerEl .clientHeight ;
315
303
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 ) )
323
307
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:
332
310
333
- ```
311
+ ![ SwipeToCompletionAnimation ] ( img/swipe-to-dismiss-state.png )
334
312
335
- parallax-animator.js:
336
313
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 ();
342
364
}
343
365
344
366
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
+ }
346
407
}
347
408
});
348
409
```
349
410
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
+
350
421
351
422
# Why Extend Animation?
352
423
@@ -465,7 +536,7 @@ registerAnimator('animation-with-local-state', class FoorAnimator extends Statef
465
536
return {
466
537
this .velocity ,
467
538
this .acceleration
468
- };
539
+ }
469
540
}
470
541
471
542
curve (velocity , accerlation , t ) {
@@ -476,27 +547,34 @@ registerAnimator('animation-with-local-state', class FoorAnimator extends Statef
476
547
477
548
# Related Concepts
478
549
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.
481
553
482
554
## ScrollTimeline
483
- [ ScrollTimeline] ( https://wicg.github.io/ scroll-animations/#scrolltimeline ) is a concept introduced in
555
+ [ ScrollTimeline] [ scroll-timeline ] is a concept introduced in
484
556
scroll-linked animation proposal. It defines an animation timeline whose time value depends on
485
557
scroll position of a scroll container. ` ScrollTimeline ` can be used an an input timeline for
486
558
worklet animations and it is the intended mechanisms to give read access to scroll position.
487
559
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
+
488
563
## 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.
500
578
501
579
# WEBIDL
502
580
@@ -508,6 +586,7 @@ the animation worklet scope.
508
586
- A sequence of timelines, the first one of which is considered primary timeline and passed to
509
587
` Animation ` constructor.
510
588
589
+
511
590
``` webidl
512
591
513
592
[Constructor (DOMString animatorName,
@@ -519,10 +598,6 @@ interface WorkletAnimation : Animation {
519
598
}
520
599
```
521
600
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
-
526
601
` AnimationEffectReadOnly ` gets a writable ` localTime ` attribute which may be used to drive the
527
602
effect from the worklet global scope.
528
603
@@ -545,3 +620,7 @@ the most recent version.
545
620
[ WA ] : https://drafts.csswg.org/web-animations/
546
621
[ animation ] : https://drafts.csswg.org/web-animations/#animations
547
622
[ 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
0 commit comments