CSS BLUEBOTTLE - 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 Bluebottle</title>
<style>
/* =====================================================================
1. TYPED CUSTOM PROPERTIES
===================================================================== */
@property --t { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* position on the flight path */
@property --q { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* slow drift that reshapes each lap */
@property --a { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* altitude cycle */
@property --w { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* hover wobble cycle */
@property --d { syntax: "<number>"; inherits: true; initial-value: 0; } /* dodge progress, 0 to 1 */
@property --beat { syntax: "<angle>"; inherits: true; initial-value: 0deg; } /* wing stroke angle */
/* =====================================================================
2. TOKENS
===================================================================== */
:root {
--u: clamp(0.8px, 0.16vmin, 1.4px); /* fly unit */
--reach: calc(var(--u) * 85); /* half the fly's span, wings included */
--G: min(15vmin, 140px); /* glass radius */
/* The flight path: an oval round the glass with two gentle waves on it.
The waves can stretch it to 1.16x wide and 1.2x tall, so the radii
are sized to keep the whole fly, wings and all, on screen. */
--Ry: calc((50vh - var(--reach) - 24px) / 1.2);
--Rx: max(0px, min(calc((50vw - var(--reach) - 24px) / 1.16), calc(var(--Ry) * 1.7)));
--wood-a: #c9925a;
--wood-b: #c28950;
--wood-c: #cf9b63;
--wood-d: #bd834d;
--gap: #4a2a12;
--blue-hi: #7aa6ff;
--blue: #2346b8;
--blue-deep: #0b1650;
--eye: #7a2a1a;
--night-o: 0;
--firefly-o: 0;
--ink: #fffdf4;
--btn-base: rgba(40, 22, 8, .34);
--on-wipe: #2a1808;
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"]) {
--night-o: 1;
--firefly-o: 1;
--ink: #e6ecf4;
--btn-base: rgba(6, 10, 20, .45);
--on-wipe: #0a0f1c;
}
}
:root[data-theme="dark"] {
--night-o: 1;
--firefly-o: 1;
--ink: #e6ecf4;
--btn-base: rgba(6, 10, 20, .45);
--on-wipe: #0a0f1c;
}
*, *::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(--wood-a);
font-family: "Avenir Next", Avenir, "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
/* Two radio buttons take turns. Each click checks the other one, which
swaps the animation name, so the dodge replays on every click. */
.toggle {
position: fixed; top: 0; left: 0;
width: 1px; height: 1px;
opacity: 0; pointer-events: none;
}
/* =====================================================================
3. SCENE: a slatted garden table, seen from above
===================================================================== */
.stage {
--P: 12vmin; /* board width */
position: fixed; inset: 0;
overflow: hidden;
isolation: isolate;
background:
/* gaps between boards, with a lit top edge and a shaded bottom edge */
repeating-linear-gradient(180deg,
var(--gap) 0 .5vmin,
rgba(255, 225, 180, .3) .5vmin .75vmin,
transparent .75vmin calc(var(--P) - .35vmin),
rgba(60, 25, 5, .25) calc(var(--P) - .35vmin) var(--P)),
/* grain: several fine stripe sets at slightly different angles */
repeating-linear-gradient(179.3deg, rgba(110, 55, 15, .10) 0 1px, transparent 1px 6px),
repeating-linear-gradient(180.6deg, rgba(110, 55, 15, .08) 0 2px, transparent 2px 11px),
repeating-linear-gradient(179.8deg, rgba(255, 235, 200, .08) 0 3px, transparent 3px 19px),
repeating-linear-gradient(180.2deg, rgba(90, 45, 10, .09) 0 1px, transparent 1px 27px),
/* each board a slightly different tone */
repeating-linear-gradient(180deg,
var(--wood-a) 0 var(--P),
var(--wood-b) var(--P) calc(var(--P) * 2),
var(--wood-c) calc(var(--P) * 2) calc(var(--P) * 3),
var(--wood-d) calc(var(--P) * 3) calc(var(--P) * 4));
background-position: 0 calc(50% + var(--P) * .5);
}
.stage > * { position: absolute; }
.knot {
width: var(--kw); height: var(--kh);
translate: -50% -50%;
border-radius: 50%;
background:
radial-gradient(closest-side, rgba(70, 30, 8, .45) 0 12%, transparent 24%),
repeating-radial-gradient(closest-side, rgba(110, 50, 15, .28) 0 1.5px, transparent 1.5px 4.5px);
-webkit-mask-image: radial-gradient(closest-side, #000 55%, transparent);
mask-image: radial-gradient(closest-side, #000 55%, transparent);
}
.knot--a { left: 17%; top: 21%; --kw: 14vmin; --kh: 4.6vmin; }
.knot--b { left: 84%; top: 77%; --kw: 11vmin; --kh: 3.6vmin; }
/* ---- The glass of lemonade ---- */
.glass-shadow, .caustic, .glass, .straw {
left: 50%; top: 50%;
}
.glass-shadow {
width: calc(var(--G) * 2.3); height: calc(var(--G) * 2.05);
translate: calc(-50% + var(--G) * .32) calc(-50% + var(--G) * .42);
rotate: 35deg;
border-radius: 50%;
background: radial-gradient(closest-side, rgba(60, 30, 5, .4), rgba(60, 30, 5, .22) 62%, transparent);
filter: blur(calc(var(--G) * .06));
}
/* Light focused through the lemonade lands as a golden patch in the shadow */
.caustic {
width: calc(var(--G) * 1.25); height: calc(var(--G) * .8);
translate: calc(-50% + var(--G) * .52) calc(-50% + var(--G) * .66);
rotate: 38deg;
border-radius: 50%;
background: radial-gradient(closest-side, rgba(255, 228, 110, .85), rgba(255, 205, 70, .35) 55%, transparent);
filter: blur(calc(var(--G) * .05));
}
.glass {
width: calc(var(--G) * 2); height: calc(var(--G) * 2);
translate: -50% -50%;
border-radius: 50%;
box-shadow: calc(var(--G) * .02) calc(var(--G) * .03) calc(var(--G) * .05) rgba(60, 30, 5, .35);
}
.liquid {
position: absolute; inset: 7%;
border-radius: 50%;
overflow: hidden;
background:
radial-gradient(ellipse 34% 22% at 34% 26%, rgba(255, 255, 255, .55), rgba(255, 255, 255, 0)),
radial-gradient(circle, rgba(255, 255, 255, .7) 0 1.2px, transparent 1.9px) 0 0 / 23px 31px,
radial-gradient(circle, rgba(255, 255, 255, .55) 0 1px, transparent 1.6px) 7px 13px / 37px 19px,
radial-gradient(circle, rgba(255, 255, 255, .5) 0 1.6px, transparent 2.3px) 15px 4px / 53px 41px,
radial-gradient(circle at 42% 38%, #fff6c4, #f6de74 50%, #ecc443 84%, #d9a92c 100%);
box-shadow: inset 0 0 calc(var(--G) * .08) rgba(180, 120, 10, .45);
}
.lemon {
position: absolute; left: 14%; top: 40%;
width: 52%; aspect-ratio: 1;
border-radius: 50%;
rotate: 18deg;
opacity: .93;
background:
radial-gradient(circle, #fff9e0 0 6%, transparent 7%),
radial-gradient(circle closest-side, transparent 0 77%, #fffbe8 78% 86%, #f2c218 87% 96%, #d9a50b 97% 100%),
radial-gradient(circle, rgba(255, 255, 255, .35) 0 1px, transparent 1.6px) 0 0 / 7px 7px,
repeating-conic-gradient(#f7dc55 0 32deg, #fff8dc 32deg 36deg);
box-shadow: 0 0 0 1px rgba(200, 150, 10, .25), 2px 3px 6px rgba(160, 110, 0, .3);
}
.ice {
position: absolute;
width: 34%; aspect-ratio: 1;
border-radius: 20%;
background:
linear-gradient(135deg, rgba(255, 255, 255, .85), rgba(235, 248, 255, .35) 38%,
rgba(255, 255, 255, .12) 62%, rgba(255, 255, 255, .55));
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, .7),
inset 3px 3px 7px rgba(255, 255, 255, .7),
inset -4px -5px 9px rgba(140, 160, 170, .3),
2px 4px 8px rgba(150, 110, 10, .25);
}
.ice::after {
content: ""; position: absolute; inset: 22%;
border-radius: 25%;
border: 1px solid rgba(255, 255, 255, .45);
rotate: 9deg;
}
.ice--a { left: 54%; top: 14%; rotate: 22deg; }
.ice--b { left: 58%; top: 52%; rotate: -14deg; width: 30%; }
.ice--c { left: 24%; top: 10%; rotate: 51deg; width: 27%; }
/* Rim: a ring of glass, bright where it faces the light */
.rim {
position: absolute; inset: 0;
border-radius: 50%;
background:
radial-gradient(circle closest-side, transparent 0 89%, rgba(255, 255, 255, .5) 91%,
rgba(255, 255, 255, .15) 95%, rgba(255, 255, 255, .7) 98%, rgba(255, 255, 255, .25) 100%),
conic-gradient(from -45deg, rgba(255, 255, 255, .95), rgba(255, 255, 255, .3) 25%,
rgba(150, 110, 50, .35) 50%, rgba(255, 255, 255, .3) 75%, rgba(255, 255, 255, .95));
-webkit-mask-image: radial-gradient(closest-side, transparent 86%, #000 87.5%);
mask-image: radial-gradient(closest-side, transparent 86%, #000 87.5%);
}
/* Straw: leans out over the rim, its open end toward the top right */
.straw {
width: calc(var(--G) * 1.4); height: calc(var(--G) * .13);
translate: calc(var(--G) * .12) -50%;
rotate: -38deg;
transform-origin: 0 50%;
border-radius: calc(var(--G) * .065);
background:
linear-gradient(180deg, rgba(255, 255, 255, .55), transparent 35%, transparent 65%, rgba(0, 40, 30, .3)),
repeating-linear-gradient(60deg, #fbfffd 0 calc(var(--G) * .07), #7fd1b9 calc(var(--G) * .07) calc(var(--G) * .13));
box-shadow: calc(var(--G) * .05) calc(var(--G) * .08) calc(var(--G) * .06) rgba(80, 50, 5, .3);
}
.straw::after {
content: ""; position: absolute;
right: 0; top: 50%;
width: calc(var(--G) * .07); height: 84%;
translate: 0 -50%;
border-radius: 50%;
background: radial-gradient(closest-side, #2f5a4e, #5f9e8b);
}
/* =====================================================================
4. FLIGHT RIG
Every motion here is a smooth function of a few angles that turn at a
constant rate, so speed and direction change continuously: no keyframe
stops, no sudden turns.
.path follows the oval and works out which way the fly is heading
.dodge adds the escape when you shoo it
.hover adds a small, smooth wobble
.shadow lies on the table, offset down-right, softer when the fly is high
.fly turns to face its heading and grows a little with altitude
===================================================================== */
.path {
left: 50%; top: 50%;
width: 0; height: 0;
/* Position: an oval plus two waves whose phase drifts slowly with --q,
so each lap is a little different from the last */
--fx: calc(var(--t) * 3 + 40deg + var(--q));
--fy: calc(var(--t) * 2 + 70deg - var(--q));
--px: calc(var(--Rx) * cos(var(--t)) + var(--Rx) * .16 * cos(var(--fx)));
--py: calc(var(--Ry) * sin(var(--t)) + var(--Ry) * .2 * sin(var(--fy)));
/* Velocity, the derivative of position. --q turns a third as fast as --t,
hence 3 + 1/3 and 2 - 1/3. atan2 turns it into a heading. */
--vx: calc(-1 * var(--Rx) * sin(var(--t)) - var(--Rx) * .16 * 3.3333 * sin(var(--fx)));
--vy: calc(var(--Ry) * cos(var(--t)) + var(--Ry) * .2 * 1.6667 * cos(var(--fy)));
--hd: calc(atan2(var(--vy), var(--vx)) + 90deg);
/* Altitude, 0.1 to 1, from two smooth waves */
--z: calc(.55 + .3 * sin(var(--a)) + .15 * sin(var(--a) * 2 + 60deg));
translate: var(--px) var(--py);
animation:
fly-t 16s linear infinite,
fly-q 48s linear infinite,
fly-a 11s linear infinite;
}
.dodge, .hover { position: absolute; left: 0; top: 0; }
/* Shoo: one smooth progress value drives the whole escape. It eases out
fast, so the dart is quick and the return is unhurried. */
.dodge {
--dir: 1;
--env: sin(var(--d) * 180deg); /* 0, up to 1, back to 0 */
--da: calc(.85 * var(--env)); /* pull toward the middle */
--db: calc(var(--dir) * .35 * sin(var(--d) * 360deg)); /* swerve one way, then the other */
--yaw: calc(var(--dir) * 65deg * var(--env)); /* turn away, then back */
--boost: var(--env); /* climb and beat harder */
/* Both terms stay inside the oval, so a dodge never leaves the screen */
translate:
calc(var(--px) * -1 * var(--da) - var(--Rx) * sin(var(--t)) * var(--db))
calc(var(--py) * -1 * var(--da) + var(--Ry) * cos(var(--t)) * var(--db));
}
#shooA:checked ~ .stage .dodge { animation: dodge-a 1.7s cubic-bezier(.2, .65, .25, 1) both; }
#shooB:checked ~ .stage .dodge { --dir: -1; animation: dodge-b 1.7s cubic-bezier(.2, .65, .25, 1) both; }
.hover {
translate:
calc(var(--u) * (1.6 * sin(var(--w)) + .8 * sin(var(--w) * 3 + 30deg)))
calc(var(--u) * (1.2 * cos(var(--w) * 2) + .7 * sin(var(--w) * 5)));
animation: fly-w 2.4s linear infinite;
--ze: min(1.3, calc(var(--z) + var(--boost) * .6));
--th: calc(var(--hd) + var(--yaw) + 1.8deg * sin(var(--w) * 2 + 20deg));
}
/* The shadow is the clearest sign of height: further off and fainter as it climbs */
.shadow {
position: absolute;
left: calc(var(--u) * -50); top: calc(var(--u) * -50);
width: calc(var(--u) * 100); height: calc(var(--u) * 100);
--sd: calc(var(--u) * (14 + var(--ze) * 70));
--sa: calc(.6 - var(--ze) * .28);
translate: calc(var(--sd) * .6) calc(var(--sd) * .8);
rotate: var(--th);
background:
radial-gradient(ellipse calc(var(--u) * (17 + var(--ze) * 4)) calc(var(--u) * (44 + var(--ze) * 4)) at 50% 52%,
rgba(40, 20, 4, var(--sa)), rgba(40, 20, 4, calc(var(--sa) * .7)) 45%, transparent),
radial-gradient(ellipse calc(var(--u) * (36 + var(--ze) * 6)) calc(var(--u) * (24 + var(--ze) * 4)) at 20% 46%,
rgba(40, 20, 4, calc(var(--sa) * .4)), transparent),
radial-gradient(ellipse calc(var(--u) * (36 + var(--ze) * 6)) calc(var(--u) * (24 + var(--ze) * 4)) at 80% 46%,
rgba(40, 20, 4, calc(var(--sa) * .4)), transparent);
}
.fly {
position: absolute;
left: calc(var(--u) * -60);
top: calc(var(--u) * -48);
width: calc(var(--u) * 120);
height: calc(var(--u) * 110);
transform-origin: calc(var(--u) * 60) calc(var(--u) * 48);
rotate: var(--th);
scale: calc(.86 + var(--ze) * .28);
/* Light from the top-left of the screen, turned into the fly's frame */
--lx: calc(-.7071 * (cos(var(--th)) + sin(var(--th))));
--ly: calc( .7071 * (sin(var(--th)) - cos(var(--th))));
}
.fly > *, .half > * { position: absolute; display: block; }
/* =====================================================================
5. ANATOMY: Calliphora vomitoria, from above
Only the right side is drawn; the left is the same markup mirrored.
===================================================================== */
.half { inset: 0; --hx: var(--lx); }
.half--l { scale: -1 1; --hx: calc(var(--lx) * -1); }
/* ---- Legs, tucked for flight: front pair reaching forward, hind pair trailing ---- */
.leg { left: calc(var(--u) * var(--ax)); top: calc(var(--u) * var(--ay)); width: 0; height: 0; rotate: var(--base); }
.leg > i, .leg > i > i, .leg > i > i > i { position: absolute; display: block; transform-origin: 0 50%; }
.leg > i {
left: 0; top: calc(var(--u) * -1.5);
width: calc(var(--u) * var(--fl)); height: calc(var(--u) * 3);
border-radius: calc(var(--u) * 1.5);
background: linear-gradient(180deg, #3a3d45, #0c0d10 55%, #000);
}
.leg > i > i {
left: calc(100% - var(--u)); top: calc(50% - var(--u) * 1.2);
width: calc(var(--u) * var(--tl)); height: calc(var(--u) * 2.4);
rotate: var(--knee);
border-radius: calc(var(--u) * 1.2);
background:
repeating-linear-gradient(90deg, transparent 0 calc(var(--u) * 2), rgba(0,0,0,.9) calc(var(--u) * 2) calc(var(--u) * 2.4)),
linear-gradient(180deg, #34363d, #08090b 55%);
}
.leg > i > i > i {
left: calc(100% - var(--u) * .6); top: calc(50% - var(--u) * .9);
width: calc(var(--u) * var(--tsl)); height: calc(var(--u) * 1.8);
rotate: var(--ankle);
border-radius: calc(var(--u) * .9);
background: repeating-linear-gradient(90deg, #26272b 0 calc(var(--u) * 2.2), #050506 calc(var(--u) * 2.2) calc(var(--u) * 2.8));
}
.leg--1 { --ax: 65; --ay: 32; --base: -55deg; --fl: 12; --tl: 13; --tsl: 9; --knee: -35deg; --ankle: -10deg; }
.leg--2 { --ax: 67; --ay: 48; --base: 35deg; --fl: 12; --tl: 12; --tsl: 8; --knee: 40deg; --ankle: 15deg; }
.leg--3 { --ax: 65; --ay: 60; --base: 62deg; --fl: 15; --tl: 17; --tsl: 11; --knee: 22deg; --ankle: 10deg; }
/* ---- Haltere: the tiny balancing knob behind the wing ---- */
.haltere {
left: calc(var(--u) * 69); top: calc(var(--u) * 57);
width: calc(var(--u) * 7); height: calc(var(--u) * 1.4);
rotate: 35deg; transform-origin: 0 50%;
background: #8a7a5a;
border-radius: var(--u);
}
.haltere::after {
content: ""; position: absolute;
right: calc(var(--u) * -2); top: 50%;
width: calc(var(--u) * 3); height: calc(var(--u) * 2.6);
translate: 0 -50%;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #e8dcb8, #9c8a62 70%);
}
/* ---- Compound eye: big, red-brown, faceted ---- */
.eye {
left: calc(var(--u) * 60.6); top: calc(var(--u) * 7);
width: calc(var(--u) * 17); height: calc(var(--u) * 22);
border-radius: 40% 60% 55% 45% / 45% 50% 50% 55%;
background:
radial-gradient(ellipse calc(var(--u) * 4) calc(var(--u) * 3)
at calc(50% + var(--hx) * 28%) calc(45% + var(--ly) * 28%), rgba(255, 255, 255, .85), rgba(255, 255, 255, 0)),
radial-gradient(ellipse 70% 60% at calc(50% + var(--hx) * 18%) calc(45% + var(--ly) * 18%), rgba(255, 140, 90, .35), transparent),
radial-gradient(circle, rgba(0, 0, 0, .28) 0 .5px, transparent 1px) 0 0 / calc(var(--u) * 1.8) calc(var(--u) * 1.8),
radial-gradient(ellipse at 40% 45%, #a8432a, var(--eye) 55%, #3b1009 100%);
box-shadow: inset calc(var(--hx) * 2 * var(--u)) calc(var(--ly) * 2 * var(--u)) calc(var(--u) * 3) rgba(30, 5, 0, .5);
}
/* ---- Wing: the blur of the stroke, plus one strobed glimpse of the blade ---- */
.wing { left: calc(var(--u) * 73); top: calc(var(--u) * 35); width: 0; height: 0; }
.wing__fan {
position: absolute;
left: calc(var(--u) * -64); top: calc(var(--u) * -64);
width: calc(var(--u) * 128); height: calc(var(--u) * 128);
border-radius: 50%;
/* Wings dwell at each end of the stroke, so the blur is densest there */
background:
conic-gradient(from 0deg,
transparent 0 calc(58deg - var(--boost) * 14deg),
rgba(255, 140, 220, .07) calc(64deg - var(--boost) * 14deg),
rgba(140, 255, 220, .06) 110deg,
rgba(150, 190, 255, .07) calc(156deg + var(--boost) * 10deg),
transparent calc(162deg + var(--boost) * 10deg) 360deg),
conic-gradient(from 0deg,
transparent 0 calc(58deg - var(--boost) * 14deg),
rgba(215, 225, 238, calc(.34 + var(--boost) * .1)) calc(64deg - var(--boost) * 14deg),
rgba(210, 220, 235, .12) 86deg 132deg,
rgba(215, 225, 238, calc(.34 + var(--boost) * .1)) calc(156deg + var(--boost) * 10deg),
transparent calc(162deg + var(--boost) * 10deg) 360deg);
-webkit-mask-image: radial-gradient(closest-side, transparent 12%, #000 32%, #000 88%, transparent 100%);
mask-image: radial-gradient(closest-side, transparent 12%, #000 32%, #000 88%, transparent 100%);
}
.wing__blade {
position: absolute;
left: 0; top: calc(var(--u) * -9);
width: calc(var(--u) * 58); height: calc(var(--u) * 18);
transform-origin: 0 50%;
rotate: var(--beat);
border-radius: 8% 75% 60% 30% / 30% 60% 70% 70%;
opacity: .3;
background:
linear-gradient(180deg, rgba(40, 30, 25, .85) 0 calc(var(--u) * 1.2), transparent calc(var(--u) * 1.8)),
linear-gradient(176deg, transparent 30%, rgba(60, 45, 35, .5) 32%, transparent 34%),
linear-gradient(172deg, transparent 52%, rgba(60, 45, 35, .45) 54%, transparent 56%),
linear-gradient(6deg, transparent 70%, rgba(60, 45, 35, .4) 72%, transparent 74%),
linear-gradient(90deg, rgba(120, 100, 80, .45), rgba(200, 210, 220, .25) 30%, rgba(220, 230, 240, .16));
/* 23ms per stroke against ~16ms per frame: every frame catches the
blade somewhere new, which reads as a buzz */
animation: beat .023s linear infinite alternate;
}
/* ---- Body, drawn once down the centre line ---- */
.abdomen {
left: calc(var(--u) * 42); top: calc(var(--u) * 58);
width: calc(var(--u) * 36); height: calc(var(--u) * 44);
border-radius: 50% 50% 50% 50% / 38% 38% 62% 62%;
background:
/* metallic shine */
radial-gradient(ellipse 34% 24% at calc(50% + var(--lx) * 26%) calc(42% + var(--ly) * 24%),
rgba(220, 235, 255, .9), rgba(220, 235, 255, 0)),
/* segment edges */
linear-gradient(180deg,
transparent 31%, rgba(4, 8, 35, .55) 32.5%, transparent 35%,
transparent 55%, rgba(4, 8, 35, .55) 56.5%, transparent 59%,
transparent 77%, rgba(4, 8, 35, .5) 78.5%, transparent 81%),
/* the silvery chequer that flickers as the fly turns */
conic-gradient(from 45deg,
rgba(205, 222, 255, calc(.07 + var(--lx) * .07)) 0 25%, transparent 0 50%,
rgba(205, 222, 255, calc(.07 + var(--lx) * .07)) 0 75%, transparent 0)
0 0 / calc(var(--u) * 6) calc(var(--u) * 6),
/* rounded edges */
radial-gradient(ellipse at 50% 45%, transparent 50%, rgba(2, 4, 25, .65) 100%),
radial-gradient(ellipse 90% 80% at calc(50% + var(--lx) * 14%) calc(40% + var(--ly) * 14%),
var(--blue-hi), var(--blue) 45%, var(--blue-deep) 100%);
box-shadow: inset 0 0 0 calc(var(--u) * .5) rgba(0, 0, 20, .5);
}
/* Bristles round the tip */
.abdomen::after {
content: ""; position: absolute;
inset: calc(var(--u) * -1.5);
border-radius: inherit;
background: repeating-conic-gradient(from 0deg at 50% 40%, rgba(0, 0, 0, .75) 0 1.5deg, transparent 1.5deg 9deg);
-webkit-mask-image: radial-gradient(ellipse 50% 50% at 50% 50%, transparent 86%, #000 90%, transparent 100%);
mask-image: radial-gradient(ellipse 50% 50% at 50% 50%, transparent 86%, #000 90%, transparent 100%);
}
.thorax {
left: calc(var(--u) * 44); top: calc(var(--u) * 26);
width: calc(var(--u) * 32); height: calc(var(--u) * 34);
border-radius: 42% 42% 38% 38% / 34% 34% 40% 40%;
background:
radial-gradient(ellipse 36% 26% at calc(50% + var(--lx) * 24%) calc(45% + var(--ly) * 24%),
rgba(220, 225, 240, .45), rgba(220, 225, 240, 0)),
/* bristles */
radial-gradient(circle, rgba(0, 0, 0, .6) 0 calc(var(--u) * .45), transparent calc(var(--u) * .8))
calc(var(--u) * 2) calc(var(--u) * 3) / calc(var(--u) * 8) calc(var(--u) * 7),
/* dark stripes on a dusty grey ground */
linear-gradient(90deg,
transparent 18%, rgba(5, 8, 15, .55) 22% 28%, transparent 32%,
transparent 44%, rgba(5, 8, 15, .55) 47% 53%, transparent 56%,
transparent 68%, rgba(5, 8, 15, .55) 72% 78%, transparent 82%),
linear-gradient(180deg, transparent 44%, rgba(0, 0, 0, .35) 46%, transparent 48%),
radial-gradient(ellipse at 50% 45%, transparent 45%, rgba(0, 0, 0, .55) 100%),
linear-gradient(180deg, #4a5160, #2a303c 60%, #1a1e27);
box-shadow: 0 calc(var(--u) * 1) calc(var(--u) * 2) rgba(0, 0, 0, .4);
}
.scutellum {
left: calc(var(--u) * 50); top: calc(var(--u) * 54);
width: calc(var(--u) * 20); height: calc(var(--u) * 11);
border-radius: 0 0 50% 50% / 0 0 100% 100%;
background:
radial-gradient(ellipse 40% 50% at calc(50% + var(--lx) * 20%) 40%, rgba(220, 225, 240, .35), transparent),
linear-gradient(180deg, #2c323e, #151922);
box-shadow: 0 calc(var(--u) * .8) calc(var(--u) * 1.2) rgba(0, 0, 0, .45);
}
.head {
left: calc(var(--u) * 43); top: calc(var(--u) * 5);
width: calc(var(--u) * 34); height: calc(var(--u) * 25);
border-radius: 50% 50% 46% 46% / 60% 60% 40% 40%;
background:
/* silvery face showing at the front */
radial-gradient(ellipse 40% 20% at 50% 4%, rgba(220, 220, 210, .8), transparent),
linear-gradient(180deg, #3a3a40, #15161a);
}
/* Ocelli: three tiny simple eyes on the crown */
.head::after {
content: ""; position: absolute;
left: 50%; top: 58%;
width: calc(var(--u) * 5); height: calc(var(--u) * 4.4);
translate: -50% 0;
background:
radial-gradient(circle at 50% 22%, #e9d9b0 0 18%, transparent 24%),
radial-gradient(circle at 18% 80%, #e9d9b0 0 18%, transparent 24%),
radial-gradient(circle at 82% 80%, #e9d9b0 0 18%, transparent 24%);
}
/* Hit area: tap the fly itself to shoo it */
.hit { left: calc(var(--u) * 30); top: 0; width: calc(var(--u) * 60); height: calc(var(--u) * 110); border-radius: 40%; cursor: pointer; }
/* Only the label for the radio that is not checked is shown */
.for-b { display: none !important; }
#shooA:checked ~ .stage .for-a, #shooA:checked ~ .ui .for-a { display: none !important; }
#shooA:checked ~ .stage .for-b { display: block !important; }
#shooA:checked ~ .ui .for-b { display: grid !important; }
/* =====================================================================
6. ATMOSPHERE (still, so the table never seems to change)
===================================================================== */
.light {
inset: 0; pointer-events: none;
mix-blend-mode: soft-light;
background: linear-gradient(135deg, rgba(255, 245, 210, .55) 0%, transparent 45%, transparent 60%, rgba(40, 15, 0, .5) 100%);
}
.night {
inset: 0; pointer-events: none;
opacity: var(--night-o);
mix-blend-mode: multiply;
background: radial-gradient(ellipse at 20% 15%, #6a7fa8, #2a3654 60%, #141b2e);
}
.moon {
inset: 0; pointer-events: none;
opacity: var(--night-o);
mix-blend-mode: screen;
background: radial-gradient(circle at 8% 6%, rgba(170, 195, 255, .3), transparent 50%);
}
.vignette {
inset: 0; pointer-events: none;
background: radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(40, 18, 4, .5) 100%);
}
.fireflies { inset: 0; opacity: var(--firefly-o); pointer-events: none; }
.firefly {
position: absolute;
width: 7px; height: 7px;
border-radius: 50%;
background: #f8ffc4;
box-shadow: 0 0 8px 3px rgba(225, 255, 120, .95), 0 0 34px 14px rgba(190, 255, 90, .32);
animation:
ff-drift var(--ft) ease-in-out infinite alternate,
ff-blink var(--fb) ease-in-out infinite;
}
.firefly:nth-child(1) { left: 12%; top: 32%; --dx: 9vmin; --dy: -7vmin; --ft: 13s; --fb: 3.4s; }
.firefly:nth-child(2) { left: 84%; top: 28%; --dx: -8vmin; --dy: 6vmin; --ft: 11s; --fb: 4.1s; animation-delay: -3s, -1.1s; }
.firefly:nth-child(3) { left: 70%; top: 74%; --dx: 7vmin; --dy: -9vmin; --ft: 15s; --fb: 2.9s; animation-delay: -6s, -2.2s; }
.firefly:nth-child(4) { left: 24%; top: 70%; --dx: -6vmin; --dy: -5vmin; --ft: 12s; --fb: 3.7s; animation-delay: -2s, -.4s; }
.firefly:nth-child(5) { left: 46%; top: 12%; --dx: 10vmin; --dy: 4vmin; --ft: 17s; --fb: 4.6s; animation-delay: -9s, -3s; }
.firefly:nth-child(6) { left: 92%; top: 60%; --dx: -9vmin; --dy: -4vmin; --ft: 14s; --fb: 3.1s; animation-delay: -4s, -1.7s; }
/* =====================================================================
7. CONTROL: same button as before; each click shoos the fly
===================================================================== */
.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);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
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 { border-color: var(--ink); }
.btn:active,
.btn:active .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 {
border-color: var(--ink);
outline: 1px solid var(--ink); outline-offset: 4px;
background-size: 100% 100%, auto;
background-position: var(--wipe-go), 0 0;
}
.toggle:focus-visible ~ .ui .btn__label {
background-size: 100% 100%, auto;
background-position: var(--wipe-go), 0 0;
}
@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; }
}
/* =====================================================================
8. KEYFRAMES
===================================================================== */
/* All linear: the smoothness comes from the sine waves, not the timing */
@keyframes fly-t { to { --t: 360deg; } }
@keyframes fly-q { to { --q: 360deg; } }
@keyframes fly-a { to { --a: 360deg; } }
@keyframes fly-w { to { --w: 360deg; } }
@keyframes beat { from { --beat: -30deg; } to { --beat: 70deg; } }
/* Shoo: two identical copies, so swapping between them restarts it */
@keyframes dodge-a { from { --d: 0; } to { --d: 1; } }
@keyframes dodge-b { from { --d: 0; } to { --d: 1; } }
@keyframes ff-drift {
0% { translate: 0 0; }
33% { translate: var(--dx) calc(var(--dy) * .4); }
66% { translate: calc(var(--dx) * .3) var(--dy); }
100% { translate: calc(var(--dx) * -.5) calc(var(--dy) * .6); }
}
@keyframes ff-blink {
0%, 50%, 100% { opacity: .22; scale: .6; }
68% { opacity: 1; scale: 1; }
84% { opacity: .4; }
}
@media (prefers-reduced-motion: reduce) {
.path, .hover, .wing__blade, .firefly { animation: none !important; }
#shooA:checked ~ .stage .dodge, #shooB:checked ~ .stage .dodge { animation-duration: .01s; }
.btn, .btn__label { transition-duration: .01s; }
}
</style>
</head>
<body>
<input type="radio" name="shoo" id="shooA" class="toggle" aria-label="Shoo the fly">
<input type="radio" name="shoo" id="shooB" class="toggle" aria-label="Shoo the fly again">
<main class="stage" aria-label="A bluebottle fly buzzing in loops above a glass of lemonade on a garden table">
<div class="knot knot--a"></div>
<div class="knot knot--b"></div>
<div class="glass-shadow"></div>
<div class="caustic"></div>
<div class="glass">
<div class="liquid">
<span class="lemon"></span>
<span class="ice ice--c"></span>
<span class="ice ice--a"></span>
<span class="ice ice--b"></span>
</div>
<span class="rim"></span>
</div>
<div class="straw"></div>
<div class="path">
<div class="dodge">
<div class="hover">
<div class="shadow"></div>
<div class="fly">
<!-- Lower layer, both sides: legs, haltere -->
<span class="half half--l">
<span class="leg leg--1"><i><i><i></i></i></i></span>
<span class="leg leg--2"><i><i><i></i></i></i></span>
<span class="leg leg--3"><i><i><i></i></i></i></span>
<span class="haltere"></span>
</span>
<span class="half half--r">
<span class="leg leg--1"><i><i><i></i></i></i></span>
<span class="leg leg--2"><i><i><i></i></i></i></span>
<span class="leg leg--3"><i><i><i></i></i></i></span>
<span class="haltere"></span>
</span>
<!-- Body -->
<span class="abdomen"></span>
<span class="thorax"></span>
<span class="scutellum"></span>
<span class="head"></span>
<!-- Upper layer, both sides: eyes, then wings over everything -->
<span class="half half--l">
<span class="eye"></span>
<span class="wing"><span class="wing__fan"></span><span class="wing__blade"></span></span>
</span>
<span class="half half--r">
<span class="eye"></span>
<span class="wing"><span class="wing__fan"></span><span class="wing__blade"></span></span>
</span>
<label class="hit for-a" for="shooA" title="Shoo the fly"></label>
<label class="hit for-b" for="shooB" title="Shoo the fly"></label>
</div>
</div>
</div>
</div>
<div class="light"></div>
<div class="night"></div>
<div class="moon"></div>
<div class="fireflies">
<span class="firefly"></span><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-a" for="shooA"><span class="btn__label">Shoo it</span></label>
<label class="btn for-b" for="shooB"><span class="btn__label">Shoo it</span></label>
</div>
</body>
</html>