@@ -996,68 +996,172 @@ Examples {#examples}
996996====================
997997
998998
999- Example 1: Twitter header. {#example-1}
1000- --------------------------
999+ Example 1: Spring timing. {#example-1}
1000+ ---------------------------------------
1001+ Here we use Animation Worklet to create animation with a custom spring timing.
1002+
1003+
1004+ <xmp class='lang-markup'>
1005+
1006+ <div id='target'></div>
1007+
1008+ <script>
1009+ await CSS.animationWorklet.addModule('spring-animator.js' );
1010+ targetEl = document.getElementById('target' );
1011+
1012+ const effect = new KeyframeEffect(
1013+ targetEl,
1014+ {transform: ['translateX(0)', 'translateX(50vw)'] },
1015+ {duration: 1000}
1016+ );
1017+ const animation = new WorkletAnimation('spring' , effect, document.timeline, {k: 2, ratio: 0.7});
1018+ animation.play();
1019+ </script>
1020+
1021+ </xmp>
1022+
1023+
1024+ <xmp class='lang-javascript'>
1025+ registerAnimator('spring' , class SpringAnimator {
1026+ constructor(options = {k: 1, ratio: 0.5}) {
1027+ this.timing = createSpring(options.k, options.ratio);
1028+ }
1029+
1030+ animate(currentTime, effect) {
1031+ let delta = this.timing(currentTime);
1032+ // scale this by target duration
1033+ delta = delta * (effect.getTimings().duration / 2);
1034+ effect.localTime = delta;
1035+ // TODO: Provide a method for animate to mark animation as finished once
1036+ // spring simulation is complete, e.g., this.finish()
1037+ // See issue https://github.com/w3c/css-houdini-drafts/issues/808
1038+ }
1039+ });
1040+
1041+ function createSpring(springConstant, ratio) {
1042+ // Normalize mass and distance to 1 and assume a reasonable init velocit
1043+ // but these can also become options to this animator.
1044+ const velocity = 0.2;
1045+ const mass = 1;
1046+ const distance = 1;
1047+
1048+ // Keep ratio < 1 to ensure it is under-damped.
1049+ ratio = Math.min(ratio, 1 - 1e-5);
1050+
1051+ const damping = ratio * 2.0 * Math.sqrt(springConstant);
1052+ const w = Math.sqrt(4.0 * springConstant - damping * damping) / (2.0 * mass);
1053+ const r = -(damping / 2.0);
1054+ const c1 = distance;
1055+ const c2 = (velocity - r * distance) / w;
1056+
1057+ // return a value in [0..distance]
1058+ return function springTiming(timeMs) {
1059+ const time = timeMs / 1000; // in seconds
1060+ const result = Math.pow(Math.E, r * time) *
1061+ (c1 * Math.cos(w * time) + c2 * Math.sin(w * time));
1062+ return distance - result;
1063+ }
1064+ }
1065+ </xmp>
1066+
1067+
1068+ Example 2: Twitter header. {#example-2}
1069+ ---------------------------------------
10011070An example of twitter profile header effect where two elements (avatar, and header) are updated in
1002- sync with scroll offset.
1071+ sync with scroll offset with an additional feature where avatar can have additional physic based
1072+ movement based on the velocity and acceleration of the scrolling.
10031073
10041074
10051075<xmp class='lang-markup'>
1006- // In document scope.
10071076<div id='scrollingContainer'>
10081077 <div id='header' style='height: 150px'></div>
10091078 <div id='avatar'><img></div>
10101079</div>
10111080
1081+ // In document scope.
10121082<script>
1013- await CSS.animationWorklet.addModule('twitter-header-animator.js' );
1014- const animation = new WorkletAnimation(
1015- 'twitter-header' ,
1016- [new KeyframeEffect($avatar, /* scales down as we scroll up */
1017- [{transform: 'scale(1)'}, {transform: 'scale(0.5)'}] ,
1018- {duration: 1000, iterations: 1}),
1019- new KeyframeEffect($header, /* loses transparency as we scroll up */
1020- [{opacity: 0}, {opacity: 0.8}] ,
1021- {duration: 1000, iterations: 1})],
1022- new ScrollTimeline({
1023- scrollSource: $scrollingContainer,
1024- orientation: 'block' ,
1025- timeRange: 1000,
1026- startScrollOffset: 0,
1027- endScrollOffset: $header.clientHeight}));
1028- animation.play();
1083+ const headerEl = document.getElementById('header' );
1084+ const avatarEl = document.getElementById('avatar' );
1085+ const scrollingContainerEl = document.getElementById('scrollingContainer' );
10291086
1030- // Since this animation is using a group effect, the same animation instance
1031- // is accessible via different handles: $avatarEl.getAnimations()[0] , $headerEl.getAnimations()[0]
10321087
1088+ const scrollTimeline = new ScrollTimeline({
1089+ scrollSource: scrollingContainerEl,
1090+ orientation: 'block' ,
1091+ timeRange: 1000,
1092+ startScrollOffset: 0,
1093+ endScrollOffset: headerEl.clientHeight
1094+ });
1095+
1096+ const effects = [
1097+ /* avatar scales down as we scroll up */
1098+ new KeyframeEffect(avatarEl,
1099+ {transform: ['scale(1)', 'scale(0.5)'] },
1100+ {duration: scrollTimeline.timeRange}),
1101+ /* header loses transparency as we scroll up */
1102+ new KeyframeEffect(headerEl,
1103+ {opacity: [0, 0.8] },
1104+ {duration: scrollTimeline.timeRange})
1105+ ];
1106+
1107+ await CSS.animationWorklet.addModule('twitter-header-animator.js' );
1108+ const animation = new WorkletAnimation('twitter-header' , effects, scrollTimeline);
1109+
1110+ animation.play();
10331111</script>
10341112
10351113</xmp>
10361114
10371115<xmp class='lang-javascript'>
10381116// Inside AnimationWorkletGlobalScope.
10391117registerAnimator('twitter-header' , class HeaderAnimator {
1040- constructor(options) {
1041- this.timing_ = new CubicBezier('ease-out' );
1118+ constructor(options, state = {velocity: 0, acceleration: 0}) {
1119+ // `state` is either undefined (first time) or it is the previous state (after an animator
1120+ // is migrated between global scopes).
1121+ this.velocity = state.velocity;
1122+ this.acceleration = state.acceleration;
10421123 }
10431124
1125+
10441126 animate(currentTime, effect) {
10451127 const scroll = currentTime; // scroll is in [0, 1000] range
10461128
1129+ if (this.prevScroll) {
1130+ this.velocity = scroll - this.prevScroll;
1131+ this.acceleration = this.velocity - this.prevVelocity;
1132+ }
1133+ this.prevScroll = scroll;
1134+ this.prevVelocity = velocity;
1135+
1136+
10471137 // Drive the output group effect by setting its children local times individually.
10481138 effect.children[0] .localTime = scroll;
1049- effect.children[1] .localTime = this.timing_(clamp(scroll, 0, 500));
1139+
1140+ effect.children[1] .localTime = curve(velocity, acceleration, scroll);
10501141 }
1142+
1143+ state() {
1144+ // Invoked before any migration attempts. The returned object must be structure clonable
1145+ // and will be passed to constructor to help animator restore its state after migration to the
1146+ // new scope.
1147+ return {
1148+ this.velocity,
1149+ this.acceleration
1150+ }
1151+ }
1152+
10511153});
10521154
1053- function clamp(value, min, max) {
1054- return Math.min(Math.max(value, min), max);
1155+ curve(scroll, velocity, acceleration) {
1156+
1157+ return /* compute an return a physical movement curve based on scroll position, and per frame
1158+ velocity and acceleration. */ ;
10551159}
10561160
10571161</xmp>
10581162
1059- Example 2 : Parallax backgrounds. {#example-2 }
1060- -----------------------------------------
1163+ Example 3 : Parallax backgrounds. {#example-3 }
1164+ ---------------------------------------------
10611165A simple parallax background example.
10621166
10631167<xmp class='lang-markup'>
@@ -1076,24 +1180,33 @@ A simple parallax background example.
10761180
10771181<script>
10781182await CSS.animationWorklet.addModule('parallax-animator.js' );
1183+
1184+ const parallaxSlowEl = document.getElementById('slow' );
1185+ const parallaxFastEl = document.getElementById('fast' );
1186+ const scrollingContainerEl = document.getElementById('scrollingContainer' );
1187+
10791188const scrollTimeline = new ScrollTimeline({
1080- scrollSource: $scrollingContainer ,
1189+ scrollSource: scrollingContainerEl ,
10811190 orientation: 'block' ,
10821191 timeRange: 1000
10831192});
1084- const scrollRange = $scrollingContainer .scrollHeight - $scrollingContainer .clientHeight;
1193+ const scrollRange = scrollingContainerEl .scrollHeight - scrollingContainerEl .clientHeight;
10851194
10861195const slowParallax = new WorkletAnimation(
10871196 'parallax' ,
1088- new KeyframeEffect($parallax_slow, [{'transform': 'translateY(0)'}, {'transform': 'translateY(' + -scrollRange + 'px)'}] , {duration: 1000}),
1197+ new KeyframeEffect(parallaxSlowEl,
1198+ {'transform' : ['translateY(0)', 'translateY(' + -scrollRange + 'px)'] },
1199+ {duration: scrollTimeline.timeRange}),
10891200 scrollTimeline,
10901201 {rate : 0.4}
10911202);
10921203slowParallax.play();
10931204
10941205const fastParallax = new WorkletAnimation(
10951206 'parallax' ,
1096- new KeyframeEffect($parallax_fast, [{'transform': 'translateY(0)'}, {'transform': 'translateY(' + -scrollRange + 'px)'}] , {duration: 1000}),
1207+ new KeyframeEffect(parallaxFastEl,
1208+ {'transform' : ['translateY(0)', 'translateY(' + -scrollRange + 'px)'] },
1209+ {duration: scrollTimeline.timeRange}),
10971210 scrollTimeline,
10981211 {rate : 0.8}
10991212);
0 commit comments