Skip to content

Commit cf112aa

Browse files
committed
[css-animation-worklet] Add new example and update the rest
1 parent d92ff9d commit cf112aa

File tree

1 file changed

+146
-33
lines changed

1 file changed

+146
-33
lines changed

css-animationworklet/Overview.bs

+146-33
Original file line numberDiff line numberDiff line change
@@ -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+
---------------------------------------
10011070
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.
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.
10391117
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;
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+
---------------------------------------------
10611165
A simple parallax background example.
10621166

10631167
<xmp class='lang-markup'>
@@ -1076,24 +1180,33 @@ A simple parallax background example.
10761180

10771181
<script>
10781182
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+
10791188
const 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

10861195
const 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
);
10921203
slowParallax.play();
10931204

10941205
const 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

Comments
 (0)