@@ -996,68 +996,172 @@ Examples {#examples}
996
996
====================
997
997
998
998
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
+ ---------------------------------------
1001
1070
An 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.
1003
1073
1004
1074
1005
1075
<xmp class='lang-markup'>
1006
- // In document scope.
1007
1076
<div id='scrollingContainer'>
1008
1077
<div id='header' style='height: 150px'></div>
1009
1078
<div id='avatar'><img></div>
1010
1079
</div>
1011
1080
1081
+ // In document scope.
1012
1082
<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' );
1029
1086
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]
1032
1087
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();
1033
1111
</script>
1034
1112
1035
1113
</xmp>
1036
1114
1037
1115
<xmp class='lang-javascript'>
1038
1116
// Inside AnimationWorkletGlobalScope.
1039
1117
registerAnimator('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;
1042
1123
}
1043
1124
1125
+
1044
1126
animate(currentTime, effect) {
1045
1127
const scroll = currentTime; // scroll is in [0, 1000] range
1046
1128
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
+
1047
1137
// Drive the output group effect by setting its children local times individually.
1048
1138
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);
1050
1141
}
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
+
1051
1153
});
1052
1154
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. */ ;
1055
1159
}
1056
1160
1057
1161
</xmp>
1058
1162
1059
- Example 2 : Parallax backgrounds. {#example-2 }
1060
- -----------------------------------------
1163
+ Example 3 : Parallax backgrounds. {#example-3 }
1164
+ ---------------------------------------------
1061
1165
A simple parallax background example.
1062
1166
1063
1167
<xmp class='lang-markup'>
@@ -1076,24 +1180,33 @@ A simple parallax background example.
1076
1180
1077
1181
<script>
1078
1182
await 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
+
1079
1188
const scrollTimeline = new ScrollTimeline({
1080
- scrollSource: $scrollingContainer ,
1189
+ scrollSource: scrollingContainerEl ,
1081
1190
orientation: 'block' ,
1082
1191
timeRange: 1000
1083
1192
});
1084
- const scrollRange = $scrollingContainer .scrollHeight - $scrollingContainer .clientHeight;
1193
+ const scrollRange = scrollingContainerEl .scrollHeight - scrollingContainerEl .clientHeight;
1085
1194
1086
1195
const slowParallax = new WorkletAnimation(
1087
1196
'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}),
1089
1200
scrollTimeline,
1090
1201
{rate : 0.4}
1091
1202
);
1092
1203
slowParallax.play();
1093
1204
1094
1205
const fastParallax = new WorkletAnimation(
1095
1206
'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}),
1097
1210
scrollTimeline,
1098
1211
{rate : 0.8}
1099
1212
);
0 commit comments