Weather — wind through reeds
Variant: Wind
The wind is never drawn; nine reeds bowing in sequence are the only evidence it exists — a force shown purely by its consequences. Each reed’s delay is its distance across the panel, so one gust front crosses the meadow in about 400ms; remove that and nine stalks bow simultaneously to nothing. The bend is a shear rather than a rotation: skewX moves each point in proportion to its height, so the tip travels furthest and the root never leaves the soil. The push is the one fast thing in the piece and the recovery is a damped swing tightening toward rest, because a gust hits harder than it leaves. Two gusts a cycle at gaps of 37% and 63% — wind on a beat becomes a metronome. Reach for it when something invisible passing through — load, traffic, a sync — deserves a presence without a number attached.
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>Weather — wind</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 — two gusts per cycle, peaking at 9% and 46%. The gaps are 37%
and 63% of the loop: wind that arrives on the beat stops being
weather and becomes a metronome. */
--cycle: 7.2s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 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);
}
.panel {
position: relative;
overflow: hidden;
width: min(392px, calc(100% - 26px));
height: clamp(150px, 46vw, 214px);
border: 1px solid var(--oat);
border-radius: calc(16px * var(--round));
background: var(--paper);
}
.meadow {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.ground {
stroke: var(--oat);
stroke-width: 1;
vector-effect: non-scaling-stroke;
opacity: 0.9;
}
.blade {
stroke: var(--olive);
stroke-width: 1.4;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
fill: none;
opacity: 0.45;
}
.stalk {
stroke: var(--olive);
stroke-width: 1.8;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
fill: none;
opacity: 0.85;
}
.seed {
fill: var(--clay-d);
opacity: 0.75;
}
/* A shear, not a rotation: skewX moves each point in proportion to its
height off the ground, so the tip travels furthest and the root never
leaves the soil — the cheapest bend there is. A rotation would swing
the reed like a stick pivoting on a nail. */
.reed {
transform-box: fill-box;
transform-origin: 50% 100%;
animation: bend var(--t) infinite;
/* Phase is a function of position — each reed waits by its distance
across the panel, so one wavefront crosses the meadow in about 400ms.
The delay is the wind's speed made visible; without it nine reeds
bow simultaneously to nothing. */
animation-delay: calc(var(--t) * var(--_p));
animation-fill-mode: backwards;
}
/* The push is the one fast thing in the piece — a gust hits harder than
it leaves — and the recovery is a damped swing, each reversal a
fraction of the last, ease-in-out throughout, because a springy stalk
decelerates into every turn. */
@keyframes bend {
0%,
4% {
transform: skewX(0);
animation-timing-function: var(--ease-out);
}
9% {
transform: skewX(calc(-14deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
13% {
transform: skewX(calc(-10.5deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
19.5% {
transform: skewX(calc(2.6deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
24% {
transform: skewX(calc(-1deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
28.5%,
41% {
transform: skewX(0);
animation-timing-function: var(--ease-out);
}
46% {
transform: skewX(calc(-8.5deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
51.5% {
transform: skewX(calc(1.7deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
56% {
transform: skewX(calc(-0.6deg * var(--_a)));
animation-timing-function: var(--ease-in-out);
}
60%,
100% {
transform: skewX(0);
}
}
.copy {
position: absolute;
right: clamp(16px, 4.6vw, 24px);
bottom: clamp(14px, 4.2vw, 22px);
left: clamp(16px, 4.6vw, 24px);
}
.head {
font-size: clamp(15px, 4.4vw, 19px);
font-weight: 600;
letter-spacing: -0.01em;
}
.sub {
margin-top: 3px;
font-size: clamp(11.5px, 3.3vw, 13px);
color: color-mix(in srgb, var(--slate) 55%, transparent);
}
@media (prefers-reduced-motion: reduce) {
/* The bend goes — a field of shearing stalks is movement with no
meaning to protect — but the wavefront survives as light: the same
per-reed delays now carry a slow brightness wave across the meadow,
so the wind still visibly crosses left to right without a single
thing changing position. */
.reed {
animation-name: pass-rm;
animation-duration: calc(var(--t) * 2);
animation-timing-function: var(--ease-in-out);
animation-delay: calc(var(--t) * var(--_p) * 8);
transform: none;
}
@keyframes pass-rm {
0%,
100% {
opacity: 0.5;
}
18% {
opacity: 1;
}
36% {
opacity: 0.5;
}
}
}
</style>
</head>
<body>
<div class="panel">
<svg class="meadow" viewBox="0 0 400 210" preserveAspectRatio="none" aria-hidden="true">
<line class="ground" x1="0" y1="178" x2="400" y2="178" />
<!-- Short static blades. They anchor the ground the way furniture
anchors a room; animate them too and the meadow has no still
point left to measure the reeds against. -->
<path class="blade" d="M22 178 q1.5 -6 0.5 -11" />
<path class="blade" d="M55 178 q-1.5 -5 -0.5 -9" />
<path class="blade" d="M113 178 q1.5 -7 0.5 -12" />
<path class="blade" d="M158 178 q-1 -5 0 -10" />
<path class="blade" d="M205 178 q1.5 -6 0.5 -10" />
<path class="blade" d="M251 178 q-1.5 -6 -0.5 -11" />
<path class="blade" d="M291 178 q1 -5 0.5 -9" />
<path class="blade" d="M338 178 q-1.5 -7 -0.5 -12" />
<path class="blade" d="M376 178 q1 -5 0 -10" />
<g transform="translate(34 178)">
<g class="reed" style="--_p: 0.006; --_a: 1.15">
<path class="stalk" d="M0 0 C 2 -20 -2 -46 2.5 -76" />
</g>
</g>
<g transform="translate(66 178)">
<g class="reed" style="--_p: 0.011; --_a: 0.9">
<path class="stalk" d="M0 0 C 1 -14 -2.5 -30 1.5 -50" />
</g>
</g>
<g transform="translate(96 178)">
<g class="reed" style="--_p: 0.016; --_a: 1.25">
<path class="stalk" d="M0 0 C 1 -22 -3 -50 1 -84" />
<ellipse class="seed" cx="1" cy="-90" rx="2.4" ry="7" />
</g>
</g>
<g transform="translate(132 178)">
<g class="reed" style="--_p: 0.023; --_a: 0.95">
<path class="stalk" d="M0 0 C -1.5 -16 2 -34 -2 -56" />
</g>
</g>
<g transform="translate(178 178)">
<g class="reed" style="--_p: 0.03; --_a: 1.05">
<path class="stalk" d="M0 0 C -1 -18 3 -40 -1 -64" />
<ellipse class="seed" cx="-1" cy="-70" rx="2.2" ry="6.5" />
</g>
</g>
<g transform="translate(224 178)">
<g class="reed" style="--_p: 0.039; --_a: 0.8">
<path class="stalk" d="M0 0 C -1 -12 2 -26 -1.5 -42" />
</g>
</g>
<g transform="translate(268 178)">
<g class="reed" style="--_p: 0.047; --_a: 1.1">
<path class="stalk" d="M0 0 C 2 -18 -2 -42 2 -70" />
</g>
</g>
<g transform="translate(310 178)">
<g class="reed" style="--_p: 0.054; --_a: 1.2">
<path class="stalk" d="M0 0 C 1 -20 -3 -46 1 -80" />
<ellipse class="seed" cx="1" cy="-86" rx="2.4" ry="7" />
</g>
</g>
<g transform="translate(352 178)">
<g class="reed" style="--_p: 0.0605; --_a: 0.85">
<path class="stalk" d="M0 0 C 1 -13 -2.5 -28 1.5 -46" />
</g>
</g>
</svg>
<div class="copy">
<p class="head">Wind through the reeds</p>
<p class="sub">Ambient weather. Nothing here reports state.</p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.