Site — sudden load
Variant: Sudden load
The same block on the same plank, twice: once lowered by the crane and once let go from a standstill, already touching. Both end on the dashed line at 8px; the released one gets there by way of 16. That factor of two is the textbook bound for a suddenly applied load — no drop height required — and it is the most useful fact a UI spring can teach: overshoot comes from how fast a change is applied, not how big it is. The lowered lane pays the load out over 864ms of ease-in-out, several of the plank’s own periods, so deflection only ever tracks the weight it already carries and there is nothing left over to ring. The released lane rings on a constant 144ms half-period, each swing 0.73 of the last, every swing a half-sine because that is a spring’s motion between extremes; the rust tick it leaves at 16px stays on the card after the ringing dies. One registered number drives everything — the block and thirteen plank segments laid on a parabola all read it — so the load and the board cannot disagree on any frame. Reach for it when a value jumps straight into a spring and someone asks where the bounce came from: ramp the target, or accept the 2×.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Site — sudden load</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — every duration in the document runs off --t. */
--cycle: 4.8s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
/* The crane's lower: slow off the mark, slow onto the plank. */
--ease-in-out: cubic-bezier(0.45, 0, 0.55, 1);
/* One swing of a ringing spring is half a sine wave — still at both
extremes, fastest through the middle. Every half-period of the
released plank uses it, and nothing else does. */
--ease-accent: cubic-bezier(0.37, 0, 0.63, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.stage {
display: grid;
gap: 10px;
width: 240px;
}
.cap {
font-size: 11px;
text-wrap: balance;
letter-spacing: 0.04em;
color: color-mix(in srgb, var(--slate) 52%, transparent);
}
.scene {
position: relative;
width: 240px;
height: 128px;
}
.lane {
position: absolute;
top: 0;
width: 112px;
height: 128px;
overflow: hidden;
}
.lane.lowered {
left: 0;
}
.lane.let-go {
left: 128px;
}
.ground {
position: absolute;
left: 0;
right: 0;
top: 108px;
height: 2px;
background: var(--oat);
}
.label {
position: absolute;
left: 0;
right: 0;
top: 114px;
font-size: 11px;
text-align: center;
color: color-mix(in srgb, var(--slate) 62%, transparent);
}
.rig {
position: absolute;
inset: 0 0 20px;
}
.trestle {
position: absolute;
top: 78px;
width: 8px;
height: 30px;
background: var(--oat);
border-radius: calc(2px * var(--round)) calc(2px * var(--round)) 0 0;
}
.trestle.l {
left: 4px;
}
.trestle.r {
left: 100px;
}
/* Where the plank's underside will rest once the block is fully on
it. Both lanes end here; only one of them gets here directly. */
.rest-mark {
position: absolute;
left: 42px;
top: 86px;
width: 28px;
height: 1.5px;
background: repeating-linear-gradient(
to right,
color-mix(in srgb, var(--slate) 34%, transparent) 0 3px,
transparent 3px 6px
);
}
/* The high-water mark: dropped in on the frame the released plank
bottoms out and left standing, so the 2x is still on the card after
the ringing has died away. */
.peak-mark {
position: absolute;
left: 44px;
top: 94px;
width: 24px;
height: 2px;
border-radius: 1px;
background: var(--rust);
opacity: 0;
animation: peak var(--t) infinite;
}
/* The deflection, as one number: 0 is unloaded, 1 is 16px of sag
at midspan. Every moving part reads it, so the block and the plank
cannot disagree on any frame. */
@property --_k {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
/* The plank is thirteen overlapping segments laid on a parabola — the
deflected shape of a loaded beam, which keeps its shape as it sags
and only changes depth. Each segment drops by k * 16px * (1 - u^2)
and turns to the curve's tangent there, u being its distance from
midspan as a fraction of the half-span; the round ends hide the
joints. */
.seg {
position: absolute;
top: 74px;
width: 12px;
height: 4px;
border-radius: 2px;
background: var(--clay-d);
transform: translateY(calc(var(--_k) * var(--_f) * 16px))
rotate(calc(var(--_k) * var(--_g) * 1deg));
}
.seg:nth-child(1) {
left: 2px;
--_f: 0.148;
--_g: 29.6;
}
.seg:nth-child(2) {
left: 10px;
--_f: 0.408;
--_g: 25.3;
}
.seg:nth-child(3) {
left: 18px;
--_f: 0.621;
--_g: 20.7;
}
.seg:nth-child(4) {
left: 26px;
--_f: 0.787;
--_g: 15.9;
}
.seg:nth-child(5) {
left: 34px;
--_f: 0.905;
--_g: 10.7;
}
.seg:nth-child(6) {
left: 42px;
--_f: 0.976;
--_g: 5.4;
}
.seg:nth-child(7) {
left: 50px;
--_f: 1;
--_g: -0;
}
.seg:nth-child(8) {
left: 58px;
--_f: 0.976;
--_g: -5.4;
}
.seg:nth-child(9) {
left: 66px;
--_f: 0.905;
--_g: -10.7;
}
.seg:nth-child(10) {
left: 74px;
--_f: 0.787;
--_g: -15.9;
}
.seg:nth-child(11) {
left: 82px;
--_f: 0.621;
--_g: -20.7;
}
.seg:nth-child(12) {
left: 90px;
--_f: 0.408;
--_g: -25.3;
}
.seg:nth-child(13) {
left: 98px;
--_f: 0.148;
--_g: -29.6;
}
.lowered .rig {
animation:
rig-cycle var(--t) linear infinite,
lower var(--t) infinite;
}
.let-go .rig {
animation:
rig-cycle var(--t) linear infinite,
ring var(--t) infinite;
}
.load {
position: absolute;
left: 43px;
top: 0;
width: 26px;
height: 74px;
transform: translateY(calc(var(--_k) * 16px));
}
.block {
position: absolute;
left: 0;
bottom: 0;
width: 26px;
height: 24px;
border-radius: calc(4px * var(--round));
background: var(--paper);
border: 1.5px solid var(--oat);
}
.cable {
position: absolute;
left: 12.25px;
top: -24px;
width: 1.5px;
height: 74px;
background: color-mix(in srgb, var(--slate) 46%, transparent);
}
/* Let go means the hook lets go: the cable whips back up out of the
way on the release frame and the block is on its own. */
.let-go .cable {
animation: release var(--t) infinite;
}
/* Lowered: the crane pays the load out over 864ms — several of the
plank's own periods — so the plank only ever feels the weight it
already has, and deflection tracks the load with nothing left over
to overshoot. Midpoint: 0 -> 8px, or k 0 -> 0.5. */
@keyframes lower {
0%,
14% {
--_k: 0;
animation-timing-function: var(--ease-in-out);
}
32%,
100% {
--_k: 0.5;
}
}
/* Let go: the same block, already touching the plank, released from
zero height. The whole weight arrives at once, nothing is holding
the plank at 8px, and it swings through to 16 — twice the static
sag, the textbook bound for a suddenly applied load (the first
swing is drawn undamped; the losses start after it). Then it rings
on a constant 144ms half-period, each swing 0.73 of the last,
around the line it would have settled on anyway:
0 -> 16, 2.2, 12.2, 4.8, 10.2, 6.4, 9.2, 7.4, 8px, written as
fractions of the 16px full scale. */
@keyframes ring {
0%,
40% {
--_k: 0;
animation-timing-function: var(--ease-accent);
}
43% {
--_k: 1;
animation-timing-function: var(--ease-accent);
}
46% {
--_k: 0.1375;
animation-timing-function: var(--ease-accent);
}
49% {
--_k: 0.7625;
animation-timing-function: var(--ease-accent);
}
52% {
--_k: 0.3;
animation-timing-function: var(--ease-accent);
}
55% {
--_k: 0.6375;
animation-timing-function: var(--ease-accent);
}
58% {
--_k: 0.4;
animation-timing-function: var(--ease-accent);
}
61% {
--_k: 0.575;
animation-timing-function: var(--ease-accent);
}
64% {
--_k: 0.4625;
animation-timing-function: var(--ease-accent);
}
67%,
100% {
--_k: 0.5;
}
}
/* The block rides the cable until 40%, so the cable's own keyframes
stay at zero until then and only have to describe the retract. */
@keyframes release {
0%,
40% {
transform: translateY(0);
animation-timing-function: var(--ease-out);
}
45%,
100% {
transform: translateY(-22px);
}
}
@keyframes peak {
0%,
42.9% {
opacity: 0;
transform: scaleX(0.4);
}
43% {
opacity: 1;
transform: scaleX(0.4);
animation-timing-function: var(--ease-out);
}
46%,
100% {
opacity: 1;
transform: scaleX(1);
}
}
/* Both lanes are reset to their unloaded pose between 93% and 94%,
inside the dark window — a load lifting itself is never shown. */
@keyframes rig-cycle {
0% {
opacity: 0;
}
3%,
88% {
opacity: 1;
}
92%,
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
/* The ringing is the vestibular part and all of it goes. What stays
is the result: both planks at the same sag, and the rust mark
showing how far the released one went to get there — breathing,
so the eye is pointed at the number that matters. */
.lowered .rig,
.let-go .rig,
.cable {
animation: none;
}
.rig {
opacity: 1;
--_k: 0.5;
}
.let-go .cable {
transform: translateY(-22px);
}
.peak-mark {
animation: rest-cue calc(var(--t) * 0.6) var(--ease-in-out) infinite alternate;
}
@keyframes rest-cue {
from {
opacity: 0.4;
}
to {
opacity: 1;
}
}
}
</style>
</head>
<body>
<div
class="stage"
role="img"
aria-label="Two identical blocks resting on identical planks: one is lowered slowly by a cable and the plank sags smoothly to its resting depth; the other is released all at once, the plank swings through to twice that depth and rings down to the same rest"
>
<p class="cap">same block, same plank · let go at once, it sags twice as far</p>
<div class="scene">
<div class="lane lowered">
<div class="ground"></div>
<div class="rig">
<span class="trestle l"></span>
<span class="trestle r"></span>
<span class="rest-mark"></span>
<div class="plank">
<span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span>
</div>
<div class="load">
<span class="cable"></span>
<span class="block"></span>
</div>
</div>
<p class="label">lowered</p>
</div>
<div class="lane let-go">
<div class="ground"></div>
<div class="rig">
<span class="trestle l"></span>
<span class="trestle r"></span>
<span class="rest-mark"></span>
<span class="peak-mark"></span>
<div class="plank">
<span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span><span class="seg"></span><span class="seg"></span
><span class="seg"></span>
</div>
<div class="load">
<span class="cable"></span>
<span class="block"></span>
</div>
</div>
<p class="label">let go</p>
</div>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.