CSS DRAGONFLY - THREEJS PROCEDURAL ANIMATION SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>CSS Dragonfly</title>
<style>
/* =====================================================================
1. TYPED CUSTOM PROPERTIES
===================================================================== */
@property --t { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* position along the flight path */
@property --g { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* glide-and-flap cycle */
@property --wp { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* wingbeat phase */
@property --land { syntax: "<number>"; inherits: true; initial-value: 0; } /* 0 flying, 1 perched */
/* =====================================================================
2. TOKENS
The camera is fixed. Everything in the 3D layers is placed in world
units: x right, y down, z toward you, with the eye at the horizon.
===================================================================== */
:root {
--P: 1000px; /* perspective: distance from eye to screen */
--hz: 36vh; /* horizon, from the top of the screen */
--Hc: 22vh; /* how far the water lies below eye level: a low camera */
--u: clamp(1.6px, .38vmin, 3px);
/* Side-to-side range of the flight, sized so it stays on screen */
--A: max(20px, calc((50vw - var(--u) * 62 - 16px) / 1.3));
--k: -.18; /* direction of the sun: 18% left of straight ahead */
/* The reed bed the dragonfly perches on */
--Hr: calc(var(--Hc) * .62);
--rz: -80px;
--rx: calc(var(--A) * -.92);
--sky-top: #3f78b8;
--sky-mid: #8fb6d9;
--sky-low: #f5c98a;
--haze: #f7d9a8;
--cloud: rgba(255, 232, 210, .55);
--hill: #8296a6;
--tree: #3b5446;
--tree-deep: #23362c;
--w-horizon: #f0cf99;
--w-mid: #6f9fb5;
--w-deep: #1f4a5e;
--road: rgba(255, 220, 150, .55);
--glint: rgba(255, 244, 205, .95);
--sun-o: 1;
--night-o: 0;
--firefly-o: 0;
--ink: #fffdf6;
--btn-base: rgba(20, 40, 55, .55);
--on-wipe: #10202a;
color-scheme: light dark;
box-sizing: border-box;
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--sky-top: #0b1430; --sky-mid: #1d2d55; --sky-low: #3a4a78; --haze: #4a5a88;
--cloud: rgba(150, 165, 210, .22);
--hill: #34405c; --tree: #18222e; --tree-deep: #0e151d;
--w-horizon: #4a5a85; --w-mid: #2a3a5c; --w-deep: #0e1a2c;
--road: rgba(200, 215, 255, .3); --glint: rgba(225, 235, 255, .9);
--sun-o: 0; --night-o: 1; --firefly-o: 1;
--ink: #e6ecf6; --btn-base: rgba(6, 10, 22, .62); --on-wipe: #0a0f1d;
}
}
:root[data-theme="dark"] {
--sky-top: #0b1430; --sky-mid: #1d2d55; --sky-low: #3a4a78; --haze: #4a5a88;
--cloud: rgba(150, 165, 210, .22);
--hill: #34405c; --tree: #18222e; --tree-deep: #0e151d;
--w-horizon: #4a5a85; --w-mid: #2a3a5c; --w-deep: #0e1a2c;
--road: rgba(200, 215, 255, .3); --glint: rgba(225, 235, 255, .9);
--sun-o: 0; --night-o: 1; --firefly-o: 1;
--ink: #e6ecf6; --btn-base: rgba(6, 10, 22, .62); --on-wipe: #0a0f1d;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-padding-top: env(safe-area-inset-top, 0px); }
html, body { height: 100%; overflow: hidden; }
body {
background: var(--w-deep);
font-family: "Avenir Next", Avenir, "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.toggle {
position: fixed; top: 0; left: 0;
width: 1px; height: 1px;
opacity: 0; pointer-events: none;
}
/* =====================================================================
3. THE FLAT BACKDROP: sky, sun, far shore, and the water's base colour
===================================================================== */
.stage { position: fixed; inset: 0; overflow: hidden; isolation: isolate; }
.stage > * { position: absolute; }
.sky {
inset: 0 0 auto 0; height: calc(var(--hz) + 2px);
background: linear-gradient(180deg, var(--sky-top) 0%, var(--sky-mid) 45%, var(--sky-low) 92%, var(--haze) 100%);
}
.stars {
inset: 0 0 auto 0; height: var(--hz);
opacity: var(--night-o);
background:
radial-gradient(circle, rgba(255,255,255,.9) 0 1px, transparent 1.6px) 0 0 / 97px 61px,
radial-gradient(circle, rgba(255,255,255,.7) 0 .8px, transparent 1.4px) 31px 17px / 53px 83px,
radial-gradient(circle, rgba(255,255,255,.6) 0 .7px, transparent 1.2px) 11px 40px / 41px 37px;
-webkit-mask-image: linear-gradient(#000 40%, transparent);
mask-image: linear-gradient(#000 40%, transparent);
}
.sun {
left: calc(50% + var(--k) * var(--P)); top: calc(var(--hz) - 7vh);
width: 9vmin; height: 9vmin;
translate: -50% -50%;
border-radius: 50%;
opacity: var(--sun-o);
background: radial-gradient(circle, #fffbe8 0 42%, #ffe2a0 62%, rgba(255, 215, 140, 0) 70%);
box-shadow: 0 0 6vmin 3vmin rgba(255, 214, 140, .7), 0 0 26vmin 12vmin rgba(255, 190, 110, .3);
}
.clouds {
inset: 0 0 auto 0; height: var(--hz);
background:
radial-gradient(ellipse 22vw 2.2vh at 22% 62%, var(--cloud), transparent),
radial-gradient(ellipse 30vw 2.8vh at 70% 55%, var(--cloud), transparent),
radial-gradient(ellipse 16vw 1.6vh at 52% 74%, var(--cloud), transparent),
radial-gradient(ellipse 26vw 3vh at 88% 30%, var(--cloud), transparent),
radial-gradient(ellipse 20vw 2.4vh at 10% 26%, var(--cloud), transparent);
filter: blur(4px);
}
/* Far hills and tree line, cut from solid colour with radial-gradient masks */
.hills, .trees { left: 0; right: 0; }
.hills {
bottom: calc(100% - var(--hz) - .5vh); height: 12vh;
background: linear-gradient(180deg, var(--hill), color-mix(in srgb, var(--hill) 65%, var(--haze)));
filter: blur(3px);
-webkit-mask-image:
radial-gradient(ellipse 30% 85% at 8% 100%, #000 98%, transparent),
radial-gradient(ellipse 22% 60% at 34% 100%, #000 98%, transparent),
radial-gradient(ellipse 26% 48% at 58% 100%, #000 98%, transparent),
radial-gradient(ellipse 30% 100% at 90% 100%, #000 98%, transparent);
mask-image:
radial-gradient(ellipse 30% 85% at 8% 100%, #000 98%, transparent),
radial-gradient(ellipse 22% 60% at 34% 100%, #000 98%, transparent),
radial-gradient(ellipse 26% 48% at 58% 100%, #000 98%, transparent),
radial-gradient(ellipse 30% 100% at 90% 100%, #000 98%, transparent);
}
.trees {
bottom: calc(100% - var(--hz) - .4vh); height: 6vh;
background: linear-gradient(180deg, var(--tree), var(--tree-deep));
filter: blur(2.5px);
--tl: linear-gradient(#000, #000) 0 100% / 100% 2.4vh no-repeat,
radial-gradient(ellipse 50% 1.4vh at 50% 3.6vh, #000 96%, transparent) 0 0 / 2.6vmin 100% repeat-x,
radial-gradient(ellipse 50% 2vh at 50% 3.8vh, #000 96%, transparent) 1.1vmin 0 / 4.3vmin 100% repeat-x,
radial-gradient(ellipse 50% 2.8vh at 50% 4vh, #000 96%, transparent) 2.7vmin 0 / 7.9vmin 100% repeat-x;
-webkit-mask: var(--tl);
mask: var(--tl);
}
/* The water's base colour: sky-bright near the horizon, deep up close,
with a warm road of light under the sun */
.water {
left: 0; right: 0; top: var(--hz); bottom: 0;
background:
radial-gradient(ellipse 12vmin 70% at calc(50% + var(--k) * var(--P)) 0%, var(--road), transparent),
linear-gradient(180deg, var(--w-horizon) 0%, var(--w-mid) 24%, var(--w-deep) 100%);
}
/* The far shore, mirrored and broken up by ripples */
.shore-mirror {
left: 0; right: 0; top: calc(var(--hz) - .1vh); height: 5.5vh;
opacity: .5;
background: linear-gradient(180deg, var(--tree-deep), var(--tree));
--sm: linear-gradient(#000, #000) 0 0 / 100% 2.4vh no-repeat,
radial-gradient(ellipse 50% 1.4vh at 50% 2.4vh, #000 96%, transparent) 0 0 / 2.6vmin 100% repeat-x,
radial-gradient(ellipse 50% 2vh at 50% 2.2vh, #000 96%, transparent) 1.1vmin 0 / 4.3vmin 100% repeat-x,
radial-gradient(ellipse 50% 2.8vh at 50% 2vh, #000 96%, transparent) 2.7vmin 0 / 7.9vmin 100% repeat-x;
-webkit-mask: var(--sm);
mask: var(--sm);
filter: blur(3px);
}
/* Ripples break the reflection into streaks */
.shore-mirror::after {
content: ""; position: absolute; inset: 0;
background: repeating-linear-gradient(180deg, transparent 0 3px, var(--w-horizon) 3px 4px);
opacity: .8;
}
/* =====================================================================
4. THE 3D LAYERS
Three views share one camera (same perspective and origin):
reflections, then the water surface, then everything in the air.
Keeping them separate lets the reflections sit under the ripples.
===================================================================== */
.view {
inset: 0;
perspective: var(--P);
perspective-origin: 50% var(--hz);
pointer-events: none;
}
.root {
position: absolute; left: 50%; top: var(--hz);
width: 0; height: 0;
transform-style: preserve-3d;
--camY: 0px;
}
/* Reflections: the same world, mirrored in the plane of the water. The
transparency goes on each part rather than the whole layer: fading a
whole 3D layer makes the browser redraw it off-screen every frame. */
.view--mirror .reed > b,
.df--mirror > * { opacity: .42; }
.view--mirror .root {
transform: translateY(calc(var(--Hc) * 2)) scaleY(-1);
--camY: calc(var(--Hc) * 2); /* the camera, as seen from inside the mirrored world */
}
/* ---- Water surface: a 2600px square laid flat, from just under the camera
out toward the horizon (local y = world z + 1920px).
No clipping and no mask on purpose: either one makes the browser redraw
the whole surface off-screen every time a ripple moves. The edges are
instead kept off-screen, and the far edge sits under the horizon haze. ---- */
.lake {
position: absolute;
left: -1300px; top: calc(var(--Hc) - 2600px);
width: 2600px; height: 2600px;
transform-origin: 50% 100%;
transform: translateZ(680px) rotateX(90deg);
}
/* Wavelets: staggered light and dark dashes, two sizes in one layer.
Perspective packs them tighter toward the horizon on its own. Drawn at
half size and scaled up 2x (a quarter of the pixels; the wavelets are
soft enough not to show it), and held still: moving water would make
the browser repaint the whole lake on every frame. The glitter does
the shimmering instead, and it only touches a narrow strip. */
.ripples {
position: absolute;
left: -900px; top: -60px;
width: 2200px; height: 1360px;
transform-origin: 0 0;
transform: scale(2);
background:
radial-gradient(ellipse 13px 1.75px at 50% 50%, rgba(255, 245, 225, .2), transparent) 0 0 / 30px 15px,
radial-gradient(ellipse 11px 1.5px at 50% 50%, rgba(0, 20, 40, .2), transparent) 15px 7.5px / 30px 15px,
radial-gradient(ellipse 30px 3px at 50% 50%, rgba(255, 245, 225, .1), transparent) 0 0 / 70px 23px,
radial-gradient(ellipse 27px 2.5px at 50% 50%, rgba(0, 20, 40, .14), transparent) 35px 11.5px / 70px 23px;
}
/* Glitter road: a band aimed at the sun. Its slope on the water equals the
sun's direction, so in perspective it narrows to a point right under it. */
.glints {
position: absolute;
left: calc(1300px + 320px * var(--k) - 80px); bottom: 0;
width: 160px; height: 1350px;
transform-origin: 50% 100%;
transform: rotate(atan(var(--k))) scale(2);
}
/* Soft edges without a mask: a dense strip down the middle, a sparser one
either side of it */
.glints::before, .glints::after {
content: ""; position: absolute; inset: 0;
animation: twinkle 1.6s ease-in-out infinite alternate;
}
.glints::before { inset: 0 30%; }
.glints::after { opacity: .7; }
.glints::before {
background:
radial-gradient(ellipse 4.5px .8px, var(--glint), transparent) 0 0 / 11.5px 6.5px,
radial-gradient(ellipse 3px .6px, var(--glint), transparent) 4.5px 2.5px / 18.5px 9.5px;
}
.glints::after {
background:
radial-gradient(ellipse 4px .75px, var(--glint), transparent) 2px 3.5px / 14.5px 8.5px,
radial-gradient(ellipse 2.5px .55px, var(--glint), transparent) 7.5px 1px / 21.5px 5.5px;
animation-delay: -1.6s;
}
/* Lily pads, laid on the water near the reeds */
.pad {
position: absolute;
width: var(--ps); height: var(--ps);
translate: -50% -50%;
rotate: var(--pr);
border-radius: 50%;
background:
radial-gradient(circle, rgba(255, 255, 255, .12) 0 5%, transparent 6%),
repeating-conic-gradient(from 0deg, rgba(20, 60, 20, .25) 0 2deg, transparent 2deg 22deg),
radial-gradient(circle at 40% 35%, #86c058, #4f8f32 60%, #2f5f1f 100%);
box-shadow: 0 0 0 3px rgba(30, 60, 30, .35);
-webkit-mask-image: conic-gradient(from 12deg, transparent 0 24deg, #000 24deg);
mask-image: conic-gradient(from 12deg, transparent 0 24deg, #000 24deg);
}
.pad--1 { left: calc(1300px + var(--rx) + 150px); top: calc(1920px + var(--rz) + 120px); --ps: 120px; --pr: 20deg; }
.pad--2 { left: calc(1300px + var(--rx) + 290px); top: calc(1920px + var(--rz) + 40px); --ps: 90px; --pr: 140deg; }
.pad--3 { left: calc(1300px + var(--rx) + 60px); top: calc(1920px + var(--rz) + 260px); --ps: 140px; --pr: 250deg; }
.pad--4 { left: calc(1300px + var(--rx) + 330px); top: calc(1920px + var(--rz) + 230px); --ps: 100px; --pr: 70deg; }
.pad--5 { left: calc(1300px - var(--rx) * 1.4); top: calc(1920px - 700px); --ps: 130px; --pr: 300deg; }
.pad--6 { left: calc(1300px - var(--rx) * 1.4 + 170px); top: calc(1920px - 760px); --ps: 95px; --pr: 190deg; }
/* A water lily: two layers of pointed petals and a gold centre */
.lily {
position: absolute;
left: calc(1300px + var(--rx) + 150px); top: calc(1920px + var(--rz) + 120px);
width: 70px; height: 70px;
translate: -50% -50%;
background:
radial-gradient(circle, #ffd54a 0 12%, #e8a91c 18%, transparent 20%),
radial-gradient(circle, #fff 0 30%, #f7d3e2 100%);
clip-path: polygon(50% 0, 58% 34%, 85% 15%, 66% 42%, 100% 50%, 66% 58%, 85% 85%, 58% 66%,
50% 100%, 42% 66%, 15% 85%, 34% 58%, 0 50%, 34% 42%, 15% 15%, 42% 34%);
}
.lily::after {
content: ""; position: absolute; inset: 14%;
rotate: 22.5deg;
background: radial-gradient(circle, #ffd54a 0 18%, #f2b72a 26%, #fff 34%, #f9e4ec 100%);
clip-path: inherit;
}
/* ---- Reeds and bulrushes: upright planes standing in the water.
The camera never moves, so flat planes facing it read as solid. ---- */
.reeds { position: absolute; transform-style: preserve-3d; }
.reed {
position: absolute; left: 0; top: 0;
width: var(--rw); height: calc(var(--Hr) * var(--rh));
transform-origin: 50% 100%;
transform:
translate3d(calc(var(--rx) + var(--dx) - var(--rw) / 2), calc(var(--Hc) - var(--Hr) * var(--rh)), calc(var(--rz) + var(--dz)))
rotate(var(--lean));
}
/* The blade sways with fixed keyframe values, so the browser can run it
off the main thread */
.reed > b {
position: absolute; inset: 0; display: block;
transform-origin: 50% 100%;
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
background: linear-gradient(90deg, #1f3a18, #4f7a2c 45%, #7fa548 55%, #2c4a1e);
animation: sway var(--sw) ease-in-out infinite alternate;
}
/* A bulrush head on some of them */
.reed--rush > b { border-radius: 2px; background: linear-gradient(90deg, #2b3f1c, #5a7a34 50%, #2b3f1c); }
.reed--rush > b::before {
content: ""; position: absolute;
left: 50%; top: 4%;
width: 260%; height: 16%;
translate: -50% 0;
border-radius: 999px;
background: linear-gradient(90deg, #3a2414, #8a5a32 45%, #a8744a 55%, #4a2e18);
}
.reed--rush > b::after {
content: ""; position: absolute;
left: 50%; top: -4%;
width: 40%; height: 9%;
translate: -50% 0;
background: #6b5a3a;
border-radius: 2px;
}
.reed:nth-child(1) { --rw: 7px; --rh: 1; --dx: 0px; --dz: 0px; --lean: -2deg; --sw: 4.2s; }
.reed:nth-child(2) { --rw: 9px; --rh: .78; --dx: -38px; --dz: 30px; --lean: -7deg; --sw: 5.1s; }
.reed:nth-child(3) { --rw: 6px; --rh: .9; --dx: 30px; --dz: -20px; --lean: 5deg; --sw: 4.7s; }
.reed:nth-child(4) { --rw: 10px; --rh: .66; --dx: 62px; --dz: 40px; --lean: 9deg; --sw: 5.6s; }
.reed:nth-child(5) { --rw: 8px; --rh: .58; --dx: -72px; --dz: -10px; --lean: -12deg; --sw: 4.4s; }
.reed:nth-child(6) { --rw: 6px; --rh: .84; --dx: -14px; --dz: -50px; --lean: 3deg; --sw: 6s; }
.reed:nth-child(7) { --rw: 9px; --rh: .5; --dx: 96px; --dz: 10px; --lean: 14deg; --sw: 5.3s; }
/* A second, smaller bed far off to the right */
.reeds--far .reed { --rz: -700px; --rx: calc(var(--A) * 1.3); --Hr: calc(var(--Hc) * .5); }
/* =====================================================================
5. THE DRAGONFLY'S FLIGHT
Position, heading, bank and pitch all come from smooth sine waves in
one angle, --t, which turns at a constant rate. Velocity and turning
are the derivatives of those waves, worked out by hand below.
The flight stays close to the camera, so the dragonfly fills the frame.
This element holds only the dragonfly, so the per-frame maths touches
nothing else.
===================================================================== */
.flyer {
position: absolute; left: 0; top: 0;
width: 0; height: 0;
transform-style: preserve-3d;
animation:
fly-t 22s linear infinite,
fly-g 9s linear infinite,
flap .1s linear infinite;
transition: --land 2.6s cubic-bezier(.45, 0, .2, 1);
/* Ease the pace a little along the lap, without ever stopping */
--s: calc(var(--t) + 17.2deg * sin(var(--t)));
/* Side to side (X), toward and away from you (Zn), and up and down (q),
plus their derivatives */
--X: calc(sin(var(--s)) + .3 * sin(var(--s) * 2 + 57deg));
--X1: calc(cos(var(--s)) + .6 * cos(var(--s) * 2 + 57deg));
--X2: calc(-1 * sin(var(--s)) - 1.2 * sin(var(--s) * 2 + 57deg));
--Zn: calc(.9 * cos(var(--s)) + .2 * cos(var(--s) * 3));
--Zn1: calc(-.9 * sin(var(--s)) - .6 * sin(var(--s) * 3));
--Zn2: calc(-.9 * cos(var(--s)) - 1.8 * cos(var(--s) * 3));
--q: calc(-.05 + .5 * sin(var(--s) * 2 + 40deg));
--q1: calc(cos(var(--s) * 2 + 40deg));
/* Distance factor (what perspective divides by). x is multiplied by it
so the side-to-side range stays on screen at every depth. */
--f: calc(1.2 - .32 * var(--Zn));
--fx: calc(var(--A) * var(--X) * var(--f));
--fy: calc(var(--Hc) * var(--q));
--fz: calc(var(--P) * (-.2 + .32 * var(--Zn)));
--vx: calc(var(--A) * (var(--X1) * var(--f) - .32 * var(--X) * var(--Zn1)));
--vy: calc(var(--Hc) * var(--q1));
--vz: calc(var(--P) * .32 * var(--Zn1));
/* Bank into turns: rate of turning = (v x a) / |v|^2 */
--turn: calc(.128 * (var(--X1) * var(--Zn2) - var(--Zn1) * var(--X2))
/ (.16 * var(--X1) * var(--X1) + .1024 * var(--Zn1) * var(--Zn1)));
--roll: clamp(-30deg, calc(var(--turn) * 16deg), 30deg);
--pitch: clamp(-18deg, atan2(calc(-1 * var(--vy)), hypot(var(--vx), var(--vz))), 18deg);
/* Perched on the tallest reed, facing right and a little toward you */
--perch-x: calc(var(--rx) - var(--Hr) * .035); /* that reed leans 2deg left */
--perch-y: calc(var(--Hc) - var(--Hr) * 1.04 - var(--u) * 3);
--perch-z: var(--rz);
/* Blend flying and perched with --land */
--L: var(--land);
--M: calc(1 - var(--land));
--x: calc(var(--fx) * var(--M) + var(--perch-x) * var(--L));
--y: calc(var(--fy) * var(--M) + var(--perch-y) * var(--L));
--z: calc(var(--fz) * var(--M) + var(--perch-z) * var(--L));
--yaw: atan2(calc(-1 * (var(--vx) * var(--M) + var(--P) * .8 * var(--L))),
calc(-1 * (var(--vz) * var(--M) + var(--P) * .6 * var(--L))));
--bank: calc(var(--roll) * var(--M));
--tilt: calc(var(--pitch) * var(--M));
/* Where the camera lies, seen from the dragonfly's own frame. The round
parts of the body turn to face it, the way a tube or a ball would. */
--dX: calc(-1 * var(--x));
--dY: calc(var(--camY) - var(--y));
--dZ: calc(var(--P) - var(--z));
--dlx: calc(cos(var(--yaw)) * var(--dX) - sin(var(--yaw)) * var(--dZ));
--phi: calc(atan2(var(--dY), var(--dlx)) - var(--bank));
/* The same, for the reflection: as if seen by a camera mirrored below the water */
--phiM: calc(atan2(calc(var(--Hc) * 2 - var(--y)), var(--dlx)) - var(--bank));
/* Wings: glide stretches alternate with bursts of flapping;
the hind pair runs about a quarter-beat ahead of the front pair */
--amp: calc((.55 + .45 * sin(var(--g))) * var(--M));
--fore: calc(-3deg * var(--M) + 2deg * var(--L) - var(--amp) * 34deg * sin(var(--wp)));
--hind: calc(-2deg * var(--M) + 2deg * var(--L) - var(--amp) * 34deg * sin(var(--wp) + 80deg));
}
.toggle:checked ~ .stage .flyer { --land: 1; animation-play-state: paused, paused, running; }
.df {
position: absolute; left: 0; top: 0;
transform-style: preserve-3d;
transform:
translate3d(var(--x), var(--y), var(--z))
rotateY(var(--yaw))
rotateX(calc(var(--tilt) * -1))
rotateZ(var(--bank));
}
.df > * { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
/* The reflection: the same body, flipped about the waterline. It shows
only the body and eyes (the wings barely register on rippled water),
and its abdomen skips the moving back stripe, which you would never see
from below anyway. */
.df--mirror {
--phi: var(--phiM);
transform:
translateY(calc(var(--Hc) * 2)) scaleY(-1)
translate3d(var(--x), var(--y), var(--z))
rotateY(var(--yaw))
rotateX(calc(var(--tilt) * -1))
rotateZ(var(--bank));
}
.df--mirror .abd {
background:
repeating-linear-gradient(90deg, transparent 0 calc(var(--u) * 7.6),
rgba(4, 14, 26, .6) calc(var(--u) * 7.6) calc(var(--u) * 8.4)),
linear-gradient(90deg, rgba(110, 200, 120, 0) 74%, rgba(110, 200, 120, .6) 92%),
linear-gradient(180deg, #0c2d45 0%, #1d6797 16%, #45b2e6 34%, #a8e8ff 50%, #45b2e6 66%, #1d6797 84%, #0c2d45 100%);
}
/* =====================================================================
6. THE DRAGONFLY: a male emperor. Forward is -z, up is -y.
The abdomen and thorax are drawn as tubes that turn about the body's
long axis to face the camera; the eyes are balls that always face it.
So the body looks rounded from every angle, not like crossed cards.
===================================================================== */
.tube {
transform: translateZ(var(--cz)) rotateZ(var(--phi)) rotateY(90deg) translate(-50%, -50%);
}
.ball {
transform:
translate3d(var(--bx), var(--by), var(--bz))
rotateZ(calc(var(--bank) * -1)) rotateX(var(--tilt)) rotateY(calc(var(--yaw) * -1))
translate(-50%, -50%);
}
/* Abdomen: tail at the left end, base at the right. The black stripe down
its back slides across the tube as the view changes, and hides when you
see it from below (as in the reflection). */
.abd {
--cz: calc(var(--u) * 35);
width: calc(var(--u) * 66); height: calc(var(--u) * 6.6);
--dpos: calc(50% - 40% * cos(var(--phi)));
--dvis: clamp(0, calc(.12 - 1.2 * sin(var(--phi))), .95);
clip-path: polygon(0 36%, 3% 28%, 68% 26%, 78% 21%, 85% 28%, 92% 10%, 100% 0, 100% 100%,
92% 90%, 85% 72%, 78% 79%, 68% 74%, 3% 72%, 0 64%);
background:
repeating-linear-gradient(90deg, transparent 0 calc(var(--u) * 7.6),
rgba(4, 14, 26, .6) calc(var(--u) * 7.6) calc(var(--u) * 8.4)),
linear-gradient(180deg, transparent calc(var(--dpos) - 14%),
rgba(8, 18, 26, var(--dvis)) calc(var(--dpos) - 7%) calc(var(--dpos) + 7%),
transparent calc(var(--dpos) + 14%)),
linear-gradient(90deg, rgba(110, 200, 120, 0) 74%, rgba(110, 200, 120, .6) 92%),
linear-gradient(180deg, #0c2d45 0%, #1d6797 16%, #45b2e6 34%, #a8e8ff 50%, #45b2e6 66%, #1d6797 84%, #0c2d45 100%);
}
/* Small claspers at the tip */
.abd::before {
content: ""; position: absolute;
left: calc(var(--u) * -3); top: 38%;
width: calc(var(--u) * 4); height: 24%;
background: linear-gradient(90deg, transparent, #10202a);
clip-path: polygon(0 0, 100% 30%, 100% 70%, 0 100%, 40% 50%);
}
/* Thorax: apple green, with dark seams */
.thx {
--cz: calc(var(--u) * -8);
width: calc(var(--u) * 23); height: calc(var(--u) * 14.5);
border-radius: 46% 40% 42% 48% / 50%;
background:
linear-gradient(112deg, transparent 26%, rgba(25, 55, 20, .5) 29% 33%, transparent 36%,
transparent 56%, rgba(25, 55, 20, .5) 59% 63%, transparent 66%),
radial-gradient(ellipse 40% 26% at 55% 50%, rgba(240, 255, 200, .5), transparent),
linear-gradient(180deg, #3d7a2a 0%, #86c84e 22%, #c8f08c 50%, #86c84e 78%, #3d7a2a 100%);
}
/* Eyes: two big faceted balls that meet on top of the head */
.eye {
width: calc(var(--u) * 15); height: calc(var(--u) * 15);
border-radius: 50%;
background:
radial-gradient(circle at 34% 30%, rgba(240, 253, 255, .95) 0 6%, transparent 13%),
radial-gradient(circle, rgba(0, 30, 50, .22) 0 .45px, transparent .9px) 0 0 / calc(var(--u) * 1.4) calc(var(--u) * 1.4),
radial-gradient(circle at 44% 40%, #7fdcfb, #2c95c8 48%, #12557c 78%, #082c42 100%);
box-shadow: 0 0 0 calc(var(--u) * .4) rgba(5, 25, 40, .35);
}
.eye--l { --bx: calc(var(--u) * -5.5); --by: calc(var(--u) * -1.5); --bz: calc(var(--u) * -25); }
.eye--r { --bx: calc(var(--u) * 5.5); --by: calc(var(--u) * -1.5); --bz: calc(var(--u) * -25); }
/* Face below and in front of the eyes */
.face {
--bx: 0px; --by: calc(var(--u) * 3); --bz: calc(var(--u) * -30);
width: calc(var(--u) * 8); height: calc(var(--u) * 7);
border-radius: 50% 50% 45% 45%;
background: radial-gradient(circle at 45% 35%, #f6f4b8, #c9cf6a 55%, #6f7a2a);
}
/* Legs, held forward under the head as they are in flight */
.legs-side, .legs-front {
--leg: #13161a;
background:
linear-gradient(58deg, transparent 46%, var(--leg) 47% 53%, transparent 54%) 0 0 / 34% 100% no-repeat,
linear-gradient(64deg, transparent 46%, var(--leg) 47% 53%, transparent 54%) 40% 0 / 34% 100% no-repeat,
linear-gradient(72deg, transparent 46%, var(--leg) 47% 53%, transparent 54%) 80% 0 / 34% 90% no-repeat;
}
.legs-side {
width: calc(var(--u) * 20); height: calc(var(--u) * 9);
transform: translate3d(0, calc(var(--u) * 3.5), calc(var(--u) * -11)) rotateY(90deg) translate(-50%, 0);
}
.legs-front {
width: calc(var(--u) * 14); height: calc(var(--u) * 8);
transform: translate3d(0, calc(var(--u) * 3.5), calc(var(--u) * -14)) translate(-50%, 0);
background:
linear-gradient(115deg, transparent 46%, var(--leg) 47% 53%, transparent 54%) 0 0 / 40% 100% no-repeat,
linear-gradient(65deg, transparent 46%, var(--leg) 47% 53%, transparent 54%) 100% 0 / 40% 100% no-repeat;
}
/* ---- Wings: four flat planes hinged on top of the thorax, flapping about
the body's long axis. The left pair is the right pair mirrored. ---- */
.hinge { transform-style: preserve-3d; }
.hinge--fore { transform: translate3d(calc(var(--u) * 2.5), calc(var(--u) * -5), calc(var(--u) * -13)) rotateZ(var(--fore)); }
.hinge--hind { transform: translate3d(calc(var(--u) * 2.5), calc(var(--u) * -5), calc(var(--u) * -3)) rotateZ(var(--hind)); }
.hinge--fore.hinge--l { transform: scaleX(-1) translate3d(calc(var(--u) * 2.5), calc(var(--u) * -5), calc(var(--u) * -13)) rotateZ(var(--fore)); }
.hinge--hind.hinge--l { transform: scaleX(-1) translate3d(calc(var(--u) * 2.5), calc(var(--u) * -5), calc(var(--u) * -3)) rotateZ(var(--hind)); }
.wing {
position: absolute; left: 0; top: 0;
width: calc(var(--u) * 48); height: calc(var(--u) * 10);
transform-origin: 0 0;
transform: rotateX(90deg) translateY(-50%);
border-radius: 18% 82% 70% 30% / 45% 60% 40% 55%;
background:
/* leading edge */
linear-gradient(180deg, rgba(40, 30, 20, .9) 0 calc(var(--u) * .6), transparent calc(var(--u) * 1)),
/* the dark mark near the tip */
linear-gradient(#3a2a10, #6a4a18) no-repeat 87% calc(var(--u) * .35) / calc(var(--u) * 3.6) calc(var(--u) * 1.4),
/* a few strong veins running out from the root */
linear-gradient(176deg, transparent 33%, rgba(50, 40, 30, .45) 34.5%, transparent 36%),
linear-gradient(179deg, transparent 52%, rgba(50, 40, 30, .4) 53.5%, transparent 55%),
/* dense cross-veining */
repeating-linear-gradient(90deg, transparent 0 calc(var(--u) * 1.3), rgba(50, 40, 30, .24) calc(var(--u) * 1.3) calc(var(--u) * 1.5)),
repeating-linear-gradient(180deg, transparent 0 calc(var(--u) * 1.7), rgba(50, 40, 30, .3) calc(var(--u) * 1.7) calc(var(--u) * 1.9)),
/* membrane, amber at the root, faintly rainbowed */
linear-gradient(90deg, rgba(190, 150, 80, .55), rgba(235, 240, 248, .3) 28%, rgba(215, 245, 238, .26) 60%, rgba(245, 220, 245, .26));
}
.wing--hind {
width: calc(var(--u) * 46); height: calc(var(--u) * 13);
border-radius: 30% 82% 70% 50% / 40% 60% 40% 60%;
}
/* =====================================================================
7. ATMOSPHERE
The moon is drawn with the sky, behind every 3D layer, so the
dragonfly always passes in front of it.
===================================================================== */
.haze {
left: 0; right: 0; top: calc(var(--hz) - 3vh); height: 15vh;
pointer-events: none;
background: linear-gradient(180deg, transparent,
color-mix(in srgb, var(--haze) 45%, transparent) 20%,
color-mix(in srgb, var(--w-horizon) 45%, transparent) 50%,
color-mix(in srgb, var(--w-mid) 25%, transparent) 72%,
transparent);
}
.night {
inset: 0; pointer-events: none;
background: linear-gradient(180deg, transparent 0%, transparent calc(var(--hz) - 12vh),
rgba(8, 16, 42, .38) calc(var(--hz) + 4vh), rgba(4, 10, 30, .46) 100%);
}
.moon {
left: calc(50% + var(--k) * var(--P)); top: calc(var(--hz) - 9vh);
width: 6vmin; height: 6vmin;
translate: -50% -50%;
border-radius: 50%;
opacity: var(--night-o);
background: radial-gradient(circle at 42% 40%, #fbfbf4, #dfe3ea 70%);
box-shadow: 0 0 4vmin 1vmin rgba(220, 230, 255, .35), 0 0 18vmin 6vmin rgba(160, 180, 240, .18);
}
.light {
inset: 0; pointer-events: none;
background: radial-gradient(ellipse 60% 50% at calc(50% + var(--k) * var(--P)) var(--hz), rgba(255, 228, 175, .22), transparent 70%);
}
/* Layers only needed at night are switched off completely by day (and the
sun at night), so nothing invisible is still being animated or drawn */
.stars, .moon, .night, .fireflies { display: none; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) :is(.stars, .moon, .night, .fireflies) { display: block; }
:root:not([data-theme="light"]) :is(.sun, .light) { display: none; }
}
:root[data-theme="dark"] :is(.stars, .moon, .night, .fireflies) { display: block; }
:root[data-theme="dark"] :is(.sun, .light) { display: none; }
.vignette {
inset: 0; pointer-events: none;
background: radial-gradient(ellipse at 50% 45%, transparent 55%, rgba(5, 15, 25, .5) 100%);
}
.fireflies { inset: 0; opacity: var(--firefly-o); pointer-events: none; }
.firefly {
position: absolute;
width: 6px; height: 6px;
border-radius: 50%;
background: #f8ffc4;
box-shadow: 0 0 8px 3px rgba(225, 255, 120, .95), 0 0 30px 12px rgba(190, 255, 90, .3);
animation:
ff-drift var(--ft) ease-in-out infinite alternate,
ff-blink var(--fb) ease-in-out infinite;
}
.firefly:nth-child(1) { left: 14%; top: 58%; --ft: 13s; --fb: 3.4s; }
.firefly:nth-child(2) { left: 24%; top: 50%; --ft: 11s; --fb: 4.1s; animation-delay: -3s, -1.1s; animation-direction: alternate-reverse, normal; }
.firefly:nth-child(3) { left: 31%; top: 62%; --ft: 15s; --fb: 2.9s; animation-delay: -6s, -2.2s; }
.firefly:nth-child(4) { left: 78%; top: 48%; --ft: 12s; --fb: 3.7s; animation-delay: -2s, -.4s; animation-direction: alternate-reverse, normal; }
.firefly:nth-child(5) { left: 86%; top: 54%; --ft: 17s; --fb: 4.6s; animation-delay: -9s, -3s; }
/* =====================================================================
8. CONTROL: same button as before; lands the dragonfly on the reeds
===================================================================== */
.ui {
position: fixed; top: 50%; right: 0;
translate: 0 -50%;
z-index: 10;
padding-right: calc(env(safe-area-inset-right, 0px) + clamp(16px, 3vw, 44px));
pointer-events: none;
}
.btn {
--wipe-rest: right center;
--wipe-go: left center;
--wipe-0: 0% 100%;
--ease: cubic-bezier(.77, 0, .18, 1);
pointer-events: auto;
display: grid;
cursor: pointer;
user-select: none; -webkit-user-select: none;
border: 1px solid color-mix(in srgb, var(--ink) 55%, transparent);
background:
linear-gradient(var(--ink), var(--ink)) no-repeat var(--wipe-rest) / var(--wipe-0),
var(--btn-base);
transition: background-size .55s var(--ease), border-color .55s var(--ease);
}
.btn__label {
grid-area: 1 / 1;
padding: 15px 28px;
font-size: 14px; font-weight: 500; line-height: 1;
letter-spacing: .04em;
text-align: center;
white-space: nowrap;
color: transparent;
background:
linear-gradient(var(--on-wipe), var(--on-wipe)) no-repeat var(--wipe-rest) / var(--wipe-0),
linear-gradient(var(--ink), var(--ink));
-webkit-background-clip: text;
background-clip: text;
transition: background-size .55s var(--ease);
}
.btn:active,
.toggle:focus-visible ~ .ui .btn { border-color: var(--ink); }
.btn:active,
.btn:active .btn__label,
.toggle:focus-visible ~ .ui .btn,
.toggle:focus-visible ~ .ui .btn__label {
background-size: 100% 100%, auto;
background-position: var(--wipe-go), 0 0;
}
@media (hover: hover) {
.btn:hover { border-color: var(--ink); }
.btn:hover,
.btn:hover .btn__label {
background-size: 100% 100%, auto;
background-position: var(--wipe-go), 0 0;
}
}
.toggle:focus-visible ~ .ui .btn { outline: 1px solid var(--ink); outline-offset: 4px; }
.when-flying { visibility: hidden; }
.toggle:checked ~ .ui .when-landed { visibility: hidden; }
.toggle:checked ~ .ui .when-flying { visibility: visible; }
@media (max-width: 600px) {
.ui { padding-right: calc(env(safe-area-inset-right, 0px) + 8px); }
.btn { --wipe-rest: center bottom; --wipe-go: center top; --wipe-0: 100% 0%; }
.btn__label { writing-mode: vertical-rl; padding: 24px 13px; }
}
/* =====================================================================
9. KEYFRAMES (all motion is continuous: linear angles into sine waves,
or gentle back-and-forth sways)
===================================================================== */
@keyframes fly-t { to { --t: 360deg; } }
@keyframes fly-g { to { --g: 360deg; } }
@keyframes flap { to { --wp: 360deg; } }
@keyframes twinkle { from { opacity: 1; } to { opacity: .15; } }
@keyframes sway { from { rotate: -1.2deg; } to { rotate: 1.2deg; } }
@keyframes ff-drift {
0% { translate: 0 0; }
33% { translate: 5vmin -1.6vmin; }
66% { translate: 1.5vmin -4vmin; }
100% { translate: -2.5vmin -2.4vmin; }
}
@keyframes ff-blink {
0%, 50%, 100% { opacity: .22; scale: .6; }
68% { opacity: 1; scale: 1; }
84% { opacity: .4; }
}
@media (prefers-reduced-motion: reduce) {
.flyer, .glints::before, .glints::after, .reed > b, .firefly { animation: none !important; }
.flyer { transition-duration: .2s; }
.btn, .btn__label { transition-duration: .01s; }
}
</style>
</head>
<body>
<input type="checkbox" id="land" class="toggle" aria-label="Land the dragonfly on the reeds">
<main class="stage" aria-label="An emperor dragonfly flying low over a lake at the end of the day">
<div class="sky"></div>
<div class="stars"></div>
<div class="clouds"></div>
<div class="sun"></div>
<div class="moon"></div>
<div class="hills"></div>
<div class="trees"></div>
<div class="water"></div>
<div class="shore-mirror"></div>
<div class="light"></div>
<!-- Reflections of the reeds, mirrored in the water -->
<div class="view view--mirror">
<div class="root">
<div class="reeds">
<div class="reed reed--rush"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div>
<div class="reed reed--rush"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div>
</div>
<div class="reeds reeds--far">
<div class="reed reed--rush"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div>
<div class="reed"><b></b></div><div class="reed"><b></b></div>
</div>
</div>
</div>
<!-- The water surface -->
<div class="view view--lake">
<div class="root">
<div class="lake">
<div class="ripples"></div>
<div class="glints"></div>
<div class="pad pad--1"></div><div class="pad pad--2"></div><div class="pad pad--3"></div>
<div class="pad pad--4"></div><div class="pad pad--5"></div><div class="pad pad--6"></div>
<div class="lily"></div>
</div>
</div>
</div>
<div class="haze"></div>
<!-- In the air: the reeds and the dragonfly, depth-sorted together -->
<div class="view view--air">
<div class="root">
<div class="reeds">
<div class="reed reed--rush"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div>
<div class="reed reed--rush"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div>
</div>
<div class="reeds reeds--far">
<div class="reed reed--rush"><b></b></div><div class="reed"><b></b></div><div class="reed"><b></b></div>
<div class="reed"><b></b></div><div class="reed"><b></b></div>
</div>
<div class="flyer"><div class="df">
<div class="abd tube"></div>
<div class="thx tube"></div>
<div class="legs-side"></div>
<div class="legs-front"></div>
<div class="face ball"></div>
<div class="eye eye--l ball"></div>
<div class="eye eye--r ball"></div>
<div class="hinge hinge--hind"><div class="wing wing--hind"></div></div>
<div class="hinge hinge--hind hinge--l"><div class="wing wing--hind"></div></div>
<div class="hinge hinge--fore"><div class="wing"></div></div>
<div class="hinge hinge--fore hinge--l"><div class="wing"></div></div>
</div>
<!-- Its reflection: driven by the same flight maths, mirrored at the waterline -->
<div class="df df--mirror">
<div class="abd tube"></div>
<div class="thx tube"></div>
<div class="eye eye--l ball"></div>
<div class="eye eye--r ball"></div>
</div></div>
</div>
</div>
<div class="night"></div>
<div class="fireflies">
<span class="firefly"></span><span class="firefly"></span><span class="firefly"></span>
<span class="firefly"></span><span class="firefly"></span>
</div>
<div class="vignette"></div>
</main>
<div class="ui">
<label class="btn" for="land">
<span class="btn__label when-landed">Let it land</span>
<span class="btn__label when-flying">Let it fly</span>
</label>
</div>
</body>
</html>