CSS LADYBIRD - 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 Ladybird</title>
<style>
/* =====================================================================
1. TYPED CUSTOM PROPERTIES
Registered so they can be animated and transitioned. Every moving part
reads from these, so one keyframe drives legs, wings and lighting.
===================================================================== */
@property --heading { syntax: "<angle>"; inherits: true; initial-value: 0deg; }
@property --walk { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --open { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --sense { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --lift { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --alert { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --stride { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --fold { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --sway { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --bob { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --flap { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --twitch { syntax: "<number>"; inherits: true; initial-value: 0; }
/* =====================================================================
2. TOKENS
The daisy sets the scale: --F is the petal-tip radius. Everything else,
the ladybird included, is a fraction of it, so the ladybird can never
outgrow the petals it walks on.
===================================================================== */
:root {
/* Petal-tip radius. The flower is always centred; the button sits on top. */
--F: min(46vmin, 440px);
--D: calc(var(--F) * .28); /* yellow disc radius */
--R: calc(var(--F) * .63); /* walking circle: middle of the petals */
--u: calc(var(--F) / 240); /* ladybird unit */
--gait: .5s;
--cycle: 26s;
--ground: #3d6b2a;
--ground-hi: #5c8f3c;
--ground-deep: #1f3a14;
--far-petal: rgba(255, 255, 255, .55);
--far-yellow: rgba(255, 205, 60, .7);
--grass: rgba(140, 190, 80, .22);
--petal: #fdfcf7;
--petal-base: #efe6c8;
--petal-tip: #f2bccb;
--petal-shade: rgba(140, 135, 100, .26);
--petal-vein: rgba(150, 140, 110, .1);
--petal-cast: rgba(60, 60, 30, .3);
--disc: #f7c51e;
--disc-core: #e3a20f;
--disc-edge: #c97f06;
--floret: rgba(130, 70, 0, .32);
--red-hi: #f4553c;
--red: #d3231b;
--red-deep: #7c0b08;
--spot: #140e0e;
--cream: #f3ecdc;
--cast: rgba(40, 45, 15, .45);
--key: rgba(255, 244, 205, .5);
--fill: rgba(0, 20, 0, .45);
--vig: rgba(10, 25, 5, .55);
--night-o: 0;
--firefly-o: 0;
--shell-bright: 1;
--ink: #fffdf4;
--btn-base: rgba(18, 32, 14, .34);
--on-wipe: #17240f;
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"]) {
--ground: #0d1d18;
--ground-hi: #18332c;
--ground-deep: #03080a;
--far-petal: rgba(180, 200, 230, .16);
--far-yellow: rgba(180, 160, 80, .16);
--grass: rgba(70, 110, 95, .14);
--petal: #c9d3e3;
--petal-base: #a9b2c4;
--petal-tip: #c3b3c8;
--petal-shade: rgba(20, 30, 60, .34);
--petal-vein: rgba(40, 50, 80, .12);
--petal-cast: rgba(0, 5, 20, .45);
--disc: #8c7a33;
--disc-core: #7a6526;
--disc-edge: #5a4718;
--floret: rgba(30, 20, 0, .4);
--cast: rgba(0, 5, 15, .6);
--key: rgba(160, 195, 255, .35);
--fill: rgba(0, 0, 12, .7);
--vig: rgba(0, 0, 0, .72);
--night-o: 1;
--firefly-o: 1;
--shell-bright: .78;
--ink: #e4eee8;
--btn-base: rgba(4, 10, 8, .42);
--on-wipe: #08120d;
}
}
:root[data-theme="dark"] {
--ground: #0d1d18;
--ground-hi: #18332c;
--ground-deep: #03080a;
--far-petal: rgba(180, 200, 230, .16);
--far-yellow: rgba(180, 160, 80, .16);
--grass: rgba(70, 110, 95, .14);
--petal: #c9d3e3;
--petal-base: #a9b2c4;
--petal-tip: #c3b3c8;
--petal-shade: rgba(20, 30, 60, .34);
--petal-vein: rgba(40, 50, 80, .12);
--petal-cast: rgba(0, 5, 20, .45);
--disc: #8c7a33;
--disc-core: #7a6526;
--disc-edge: #5a4718;
--floret: rgba(30, 20, 0, .4);
--cast: rgba(0, 5, 15, .6);
--key: rgba(160, 195, 255, .35);
--fill: rgba(0, 0, 12, .7);
--vig: rgba(0, 0, 0, .72);
--night-o: 1;
--firefly-o: 1;
--shell-bright: .78;
--ink: #e4eee8;
--btn-base: rgba(4, 10, 8, .42);
--on-wipe: #08120d;
}
*, *::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(--ground-deep);
font-family: "Avenir Next", Avenir, "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
/* The state machine: a visually hidden checkbox, toggled by two labels */
.toggle {
position: fixed; top: 0; left: 0;
width: 1px; height: 1px;
opacity: 0; pointer-events: none;
}
/* =====================================================================
3. SCENE: a lawn, out of focus, with one daisy seen from above
===================================================================== */
.stage {
position: fixed; inset: 0;
overflow: hidden;
isolation: isolate;
background: radial-gradient(ellipse at 50% 45%, var(--ground-hi), var(--ground) 55%, var(--ground-deep) 100%);
}
.stage > * { position: absolute; }
/* Distant daisies and grass, blurred like a shallow depth of field */
.lawn {
inset: -6%;
background:
radial-gradient(circle at 9% 15%, var(--far-yellow) 0 1.1vmax, var(--far-petal) 1.5vmax 3.6vmax, transparent 4vmax),
radial-gradient(circle at 91% 11%, var(--far-yellow) 0 .8vmax, var(--far-petal) 1.1vmax 2.7vmax, transparent 3vmax),
radial-gradient(circle at 95% 84%, var(--far-yellow) 0 1.3vmax, var(--far-petal) 1.7vmax 4.2vmax, transparent 4.6vmax),
radial-gradient(circle at 5% 88%, var(--far-yellow) 0 .9vmax, var(--far-petal) 1.2vmax 3vmax, transparent 3.4vmax),
radial-gradient(circle at 76% 3%, var(--far-yellow) 0 .6vmax, var(--far-petal) .8vmax 2vmax, transparent 2.3vmax),
radial-gradient(circle at 24% 97%, var(--far-yellow) 0 .7vmax, var(--far-petal) .9vmax 2.3vmax, transparent 2.6vmax),
repeating-linear-gradient(112deg, transparent 0 2.2vmax, var(--grass) 2.2vmax 2.6vmax, transparent 2.6vmax 3.9vmax),
repeating-linear-gradient(64deg, transparent 0 3.1vmax, var(--grass) 3.1vmax 3.4vmax, transparent 3.4vmax 5.3vmax);
filter: blur(14px);
}
.flower {
left: 50%; top: 50%;
width: calc(var(--F) * 2); height: calc(var(--F) * 2);
translate: -50% -50%;
}
.flower__shadow {
position: absolute; inset: 2%;
translate: 4% 6%;
border-radius: 50%;
background: radial-gradient(closest-side, rgba(0,0,0,.42), rgba(0,0,0,.2) 70%, transparent);
filter: blur(14px);
}
/* Each span is one long pill through the centre: two opposite petals */
.ring { position: absolute; inset: 0; }
.ring--back { rotate: 7.5deg; scale: .985; filter: brightness(.9); }
.ring--front { filter: drop-shadow(calc(var(--F) * .008) calc(var(--F) * .014) calc(var(--F) * .012) var(--petal-cast)); }
.ring > span {
--pw: calc(var(--F) * .12);
position: absolute; left: 50%; top: 50%;
width: var(--pw);
height: calc(var(--F) * 2 * var(--len));
translate: -50% -50%;
rotate: var(--a);
border-radius: calc(var(--pw) / 2);
background:
repeating-linear-gradient(90deg,
transparent 0 calc(var(--pw) * .16), var(--petal-vein) calc(var(--pw) * .16) calc(var(--pw) * .19)),
linear-gradient(90deg, var(--petal-shade) 0%, transparent 24%, transparent 68%, var(--petal-shade) 100%),
linear-gradient(180deg,
var(--petal-tip) 0%, var(--petal) 6%, var(--petal) 36%, var(--petal-base) 50%,
var(--petal) 64%, var(--petal) 94%, var(--petal-tip) 100%);
}
/* Slightly uneven angles and lengths so it reads as grown, not drawn */
.ring > span:nth-child(1) { --a: 0deg; --len: 1; }
.ring > span:nth-child(2) { --a: 15.6deg; --len: .97; }
.ring > span:nth-child(3) { --a: 29.4deg; --len: .99; }
.ring > span:nth-child(4) { --a: 45.8deg; --len: .96; }
.ring > span:nth-child(5) { --a: 60.3deg; --len: 1; }
.ring > span:nth-child(6) { --a: 74.6deg; --len: .98; }
.ring > span:nth-child(7) { --a: 90.8deg; --len: .97; }
.ring > span:nth-child(8) { --a: 104.4deg; --len: 1; }
.ring > span:nth-child(9) { --a: 120.5deg; --len: .98; }
.ring > span:nth-child(10) { --a: 135.2deg; --len: .96; }
.ring > span:nth-child(11) { --a: 149.4deg; --len: .99; }
.ring > span:nth-child(12) { --a: 165.6deg; --len: .97; }
/* The yellow disc: a dome of tiny florets */
.disc {
--fs: calc(var(--D) * .09);
position: absolute; left: 50%; top: 50%;
width: calc(var(--D) * 2); height: calc(var(--D) * 2);
translate: -50% -50%;
border-radius: 50%;
background:
radial-gradient(circle at 36% 32%, rgba(255,255,225,.6), rgba(255,255,225,0) 40%),
radial-gradient(circle at 54% 56%, transparent 52%, rgba(110,55,0,.45) 100%),
repeating-conic-gradient(from 0deg, rgba(120,60,0,.08) 0 2.5deg, transparent 2.5deg 5deg),
radial-gradient(circle, var(--floret) 0 26%, transparent 34%) 0 0 / var(--fs) var(--fs),
radial-gradient(circle, var(--floret) 0 26%, transparent 34%) calc(var(--fs) / 2) calc(var(--fs) / 2) / var(--fs) var(--fs),
radial-gradient(circle at 50% 50%, var(--disc-core) 0 16%, var(--disc) 46%, var(--disc-edge) 100%);
box-shadow:
calc(var(--D) * .05) calc(var(--D) * .08) calc(var(--D) * .14) rgba(90, 60, 0, .4),
inset 0 0 0 calc(var(--D) * .03) rgba(255, 230, 120, .35);
}
/* =====================================================================
4. MOTION RIG
.orbit owns the heading, .beetle owns the behaviour timeline,
.flight owns body sway, hover bob and the cast shadow.
===================================================================== */
.orbit {
left: 50%; top: 50%;
width: 0; height: 0;
rotate: var(--heading);
will-change: transform;
animation: patrol var(--cycle) infinite;
}
.track { position: absolute; left: 0; top: 0; translate: var(--R) 0; }
.beetle {
position: absolute;
left: calc(var(--u) * -50);
top: calc(var(--u) * -62);
width: calc(var(--u) * 100);
height: calc(var(--u) * 124);
display: block;
cursor: pointer;
--lift: 0;
/* Light sits at the top-left of the screen. Rotate it into the
ladybird's own frame so the shine stays put while the body turns. */
--lx: calc(-.7071 * (cos(var(--heading)) + sin(var(--heading))));
--ly: calc( .7071 * (sin(var(--heading)) - cos(var(--heading))));
/* Shadow falls down-right on screen whatever the heading */
--sd: calc(var(--u) * (6 + var(--lift) * 28));
--shx: calc(var(--sd) * (.6 * cos(var(--heading)) + .8 * sin(var(--heading))));
--shy: calc(var(--sd) * (.8 * cos(var(--heading)) - .6 * sin(var(--heading))));
--shb: calc(var(--u) * (3 + var(--lift) * 12));
animation: routine var(--cycle) infinite;
transition: --lift .9s cubic-bezier(.3, .7, .2, 1);
}
/* Startled: take off, hover in place, pause the patrol */
.toggle:checked ~ .stage .beetle { --lift: 1; animation-play-state: paused; }
.toggle:checked ~ .stage .orbit { animation-play-state: paused; }
.ao {
position: absolute; display: block;
left: calc(var(--u) * 6); top: calc(var(--u) * 26);
width: calc(var(--u) * 88); height: calc(var(--u) * 92);
border-radius: 50%;
background: radial-gradient(closest-side, var(--cast), transparent);
opacity: calc(.85 - var(--lift) * .85);
filter: blur(calc(var(--u) * 3));
}
.flight {
position: absolute; inset: 0; display: block;
--o: max(var(--open), var(--lift));
--fan: max(var(--sense), var(--alert), var(--lift));
/* On take-off it drifts a little toward the middle of the flower */
translate: calc(var(--u) * -14 * var(--lift)) calc(var(--u) * -6 * var(--lift) * (1 + var(--bob)));
rotate: calc(var(--sway) * var(--walk) * (1 - var(--lift)) * 1.4deg);
scale: calc(1 + var(--lift) * .15);
filter: brightness(var(--shell-bright)) drop-shadow(var(--shx) var(--shy) var(--shb) var(--cast));
transition: --alert .35s ease;
animation:
sway var(--gait) ease-in-out infinite,
bob 1.1s ease-in-out infinite alternate;
}
.beetle:hover .flight { --alert: 1; }
/* =====================================================================
5. ANATOMY: Coccinella septempunctata
Only the right side is drawn. The left side is the same markup in a
mirrored wrapper, with its light direction flipped to match.
===================================================================== */
.half { position: absolute; inset: 0; display: block; --hx: var(--lx); }
.half--l { scale: -1 1; --hx: calc(var(--lx) * -1); }
/* ---- Legs: short, mostly hidden under the dome, tripod gait ---- */
.leg {
position: absolute; display: block;
left: calc(var(--u) * var(--ax));
top: calc(var(--u) * var(--ay));
width: 0; height: 0;
rotate: calc(var(--base)
+ var(--stride) * var(--amp) * var(--walk) * (1 - var(--lift))
+ var(--lift) * var(--tuck));
animation: step var(--gait) infinite;
}
.leg--1 { --ax: 61; --ay: 44; --base: -48deg; --amp: 12deg; --tuck: -12deg;
--fl: 17; --tl: 15; --tsl: 9; --knee: -30deg; --kfold: -14deg; --ktuck: -55deg; --ankle: -12deg; }
.leg--2 { --ax: 63; --ay: 60; --base: 6deg; --amp: 14deg; --tuck: 20deg;
--fl: 18; --tl: 17; --tsl: 9; --knee: 34deg; --kfold: 14deg; --ktuck: 70deg; --ankle: 14deg; }
.leg--3 { --ax: 62; --ay: 76; --base: 40deg; --amp: 12deg; --tuck: 24deg;
--fl: 19; --tl: 18; --tsl: 10; --knee: 28deg; --kfold: 12deg; --ktuck: 60deg; --ankle: 10deg; }
/* Tripod: R1 + L2 + R3 step together, then L1 + R2 + L3 */
.half--r .leg--2,
.half--l .leg--1,
.half--l .leg--3 { animation-delay: calc(var(--gait) * -.5); }
.femur, .tibia, .tarsus { position: absolute; display: block; transform-origin: 0 50%; }
.femur {
left: 0; top: calc(var(--u) * -2);
width: calc(var(--u) * var(--fl)); height: calc(var(--u) * 4);
border-radius: calc(var(--u) * 2);
background: linear-gradient(180deg, #3d3d40, #121213 50%, #000);
}
.tibia {
left: calc(100% - var(--u) * 1.6); top: calc(50% - var(--u) * 1.6);
width: calc(var(--u) * var(--tl)); height: calc(var(--u) * 3.2);
border-radius: calc(var(--u) * 1.6);
background: linear-gradient(180deg, #38383b, #0e0e0f 50%, #000);
rotate: calc(var(--knee) + var(--fold) * var(--kfold) * var(--walk) + var(--lift) * var(--ktuck));
}
.tarsus {
left: calc(100% - var(--u) * .8); top: calc(50% - var(--u) * 1.1);
width: calc(var(--u) * var(--tsl)); height: calc(var(--u) * 2.2);
border-radius: calc(var(--u) * 1.1);
background: repeating-linear-gradient(90deg, #2c2622 0 calc(var(--u) * 2.6), #0b0908 calc(var(--u) * 2.6) calc(var(--u) * 3.2));
rotate: calc(var(--ankle) + var(--lift) * var(--ktuck) * .4);
}
.tarsus::after {
content: ""; position: absolute;
left: calc(100% - var(--u) * .6); top: 50%;
width: calc(var(--u) * 3); height: calc(var(--u) * 3.4);
translate: 0 -50%;
background: #070605;
clip-path: polygon(0 38%, 100% 0, 30% 50%, 100% 100%, 0 62%);
}
/* ---- Abdomen: only seen when the wing cases lift ---- */
.abdomen {
position: absolute; display: block;
left: calc(var(--u) * 49.4); top: calc(var(--u) * 36);
width: calc(var(--u) * 40.6); height: calc(var(--u) * 76);
border-radius: 0 100% 100% 0 / 0 50% 50% 0;
background:
repeating-linear-gradient(180deg, transparent 0 calc(var(--u) * 10), rgba(0,0,0,.5) calc(var(--u) * 10) calc(var(--u) * 11.2)),
radial-gradient(ellipse at 0% 40%, #4a2a18, #1e120b 60%, #0a0605);
}
/* ---- Hind wing: amber membrane, folded under the wing case ---- */
.wing {
position: absolute; display: block;
left: calc(var(--u) * 54); top: calc(var(--u) * 31);
width: calc(var(--u) * 80); height: calc(var(--u) * 30);
transform-origin: 0 50%;
rotate: calc(112deg - var(--o) * 118deg + var(--o) * var(--flap) * 18deg);
scale: calc(.4 + var(--o) * .6 * (.8 + var(--flap) * .2)) calc(.55 + var(--o) * .45);
opacity: calc(var(--o) * 1.5);
border-radius: 6% 82% 62% 12% / 30% 58% 80% 70%;
background:
linear-gradient(180deg, rgba(60,30,12,.95) 0 calc(var(--u) * 1.8), transparent calc(var(--u) * 2.6)),
linear-gradient(170deg, transparent 36%, rgba(70,38,16,.7) 37.5%, transparent 39%),
linear-gradient(8deg, transparent 60%, rgba(70,38,16,.6) 61.5%, transparent 63%),
repeating-linear-gradient(80deg, transparent 0 calc(var(--u) * 14), rgba(80,45,20,.42) calc(var(--u) * 14) calc(var(--u) * 14.7)),
linear-gradient(90deg, rgba(110,62,28,.85), rgba(170,120,75,.6) 35%, rgba(185,145,105,.5) 70%, rgba(195,160,135,.42));
box-shadow: inset 0 0 0 calc(var(--u) * .6) rgba(70,38,16,.55);
animation: flap .07s linear infinite alternate;
}
.wing::after {
content: ""; position: absolute; inset: 0;
border-radius: inherit;
transform-origin: 0 50%;
rotate: calc(var(--flap) * -14deg);
background: rgba(235, 215, 195, .26);
filter: blur(calc(var(--u) * 2.5));
}
/* ---- Elytron: one half of the red dome, with its three and a half spots.
The highlight is placed in dome coordinates, so the two halves share a
single shine rather than each getting their own. ---- */
.elytron {
--cy: calc(var(--u) * 43); /* dome centre, in this element's space */
position: absolute; display: block;
left: calc(var(--u) * 49.6); top: calc(var(--u) * 30);
width: calc(var(--u) * 44.4); height: calc(var(--u) * 86);
border-radius: 0 100% 100% 0 / 0 50% 50% 0;
transform-origin: 4% 10%;
rotate: calc(var(--o) * -42deg);
scale: calc(1 - var(--o) * .22) 1;
translate: calc(var(--o) * var(--u) * 4) calc(var(--o) * var(--u) * -4);
background:
radial-gradient(ellipse calc(var(--u) * 9) calc(var(--u) * 6.5)
at calc(var(--hx) * var(--u) * 22) calc(var(--cy) + var(--ly) * var(--u) * 24),
rgba(255,255,255,.95), rgba(255,255,255,0)),
radial-gradient(ellipse calc(var(--u) * 24) calc(var(--u) * 18)
at calc(var(--hx) * var(--u) * 15) calc(var(--cy) + var(--ly) * var(--u) * 17),
rgba(255,235,230,.32), rgba(255,255,255,0)),
radial-gradient(ellipse calc(var(--u) * 48) calc(var(--u) * 47)
at calc(var(--hx) * var(--u) * -6) calc(var(--cy) + var(--ly) * var(--u) * -6),
transparent 52%, rgba(70,0,0,.42) 84%, rgba(30,0,0,.75) 100%),
radial-gradient(circle calc(var(--u) * 8) at 0 calc(var(--u) * 16), var(--spot) 90%, transparent 100%),
radial-gradient(circle calc(var(--u) * 6.5) at calc(var(--u) * 24) calc(var(--u) * 21), var(--spot) 90%, transparent 100%),
radial-gradient(circle calc(var(--u) * 9) at calc(var(--u) * 29) calc(var(--u) * 45), var(--spot) 91%, transparent 100%),
radial-gradient(circle calc(var(--u) * 7) at calc(var(--u) * 16) calc(var(--u) * 67), var(--spot) 90%, transparent 100%),
radial-gradient(ellipse calc(var(--u) * 62) calc(var(--u) * 62)
at calc(var(--hx) * var(--u) * 12) calc(var(--u) * 40 + var(--ly) * var(--u) * 12),
var(--red-hi), var(--red) 45%, var(--red-deep) 100%);
box-shadow:
inset calc(var(--u) * .8) 0 0 rgba(0,0,0,.45),
inset calc(var(--hx) * 3 * var(--u)) calc(var(--ly) * 3 * var(--u)) calc(var(--u) * 6) rgba(40,0,0,.45),
0 0 0 calc(var(--u) * .4) rgba(40,0,0,.45);
}
/* ---- Centre pieces (drawn once) ---- */
.head, .pronotum { position: absolute; display: block; }
.head {
left: calc(var(--u) * 37); top: calc(var(--u) * 9);
width: calc(var(--u) * 26); height: calc(var(--u) * 17);
border-radius: 50% 50% 40% 40% / 70% 70% 30% 30%;
background:
radial-gradient(ellipse calc(var(--u) * 3) calc(var(--u) * 2)
at calc(50% + var(--lx) * 28%) calc(45% + var(--ly) * 30%), rgba(255,255,255,.8), rgba(255,255,255,0)),
radial-gradient(circle calc(var(--u) * 2.4) at 30% 50%, var(--cream) 80%, transparent 100%),
radial-gradient(circle calc(var(--u) * 2.4) at 70% 50%, var(--cream) 80%, transparent 100%),
linear-gradient(180deg, #2c2c2e, #0a0a0b 70%);
}
/* Black shield with the two cream patches at its front corners */
.pronotum {
left: calc(var(--u) * 22); top: calc(var(--u) * 21);
width: calc(var(--u) * 56); height: calc(var(--u) * 21);
border-radius: 48% 48% 26% 26% / 78% 78% 32% 32%;
background:
radial-gradient(ellipse calc(var(--u) * 6) calc(var(--u) * 3)
at calc(50% + var(--lx) * 26%) calc(55% + var(--ly) * 40%), rgba(255,255,255,.85), rgba(255,255,255,0)),
radial-gradient(ellipse 11% 27% at 13% 46%, var(--cream) 82%, transparent 100%),
radial-gradient(ellipse 11% 27% at 87% 46%, var(--cream) 82%, transparent 100%),
radial-gradient(ellipse 60% 70% at 50% 70%, transparent 55%, rgba(0,0,0,.4) 100%),
linear-gradient(180deg, #2a2a2c, #0b0b0c 60%, #000);
box-shadow:
0 calc(var(--u) * 1) calc(var(--u) * 2) rgba(0,0,0,.45),
inset calc(var(--lx) * -2 * var(--u)) calc(var(--ly) * -2 * var(--u)) calc(var(--u) * 3) rgba(255,255,255,.16);
}
/* ---- Eye ---- */
.eye {
position: absolute; display: block;
left: calc(var(--u) * 58); top: calc(var(--u) * 13);
width: calc(var(--u) * 6); height: calc(var(--u) * 8);
border-radius: 50%;
background:
radial-gradient(circle at calc(50% + var(--hx) * 25%) calc(50% + var(--ly) * 25%),
rgba(255,255,255,.85) 0 12%, transparent 22%),
radial-gradient(circle, #3b3530, #0b0908 75%);
}
/* ---- Antenna: short stalk with a small club ---- */
.antenna {
position: absolute; display: block;
left: calc(var(--u) * 57); top: calc(var(--u) * 1);
width: calc(var(--u) * 1.8); height: calc(var(--u) * 11);
border-radius: var(--u);
transform-origin: 50% 100%;
rotate: calc(36deg + var(--twitch) * 9deg + var(--fan) * 16deg);
background: linear-gradient(90deg, #2a1f18, #4a3a2e 50%, #1a130e);
animation: twitch 3.1s ease-in-out infinite;
}
.antenna::after {
content: ""; position: absolute;
left: 50%; top: calc(var(--u) * -3.5);
width: calc(var(--u) * 3.6); height: calc(var(--u) * 5.5);
translate: -50% 0;
border-radius: 50% 50% 40% 40%;
background: radial-gradient(circle at 40% 35%, #5a4a3e, #140f0b 70%);
}
.half--l .antenna { animation-delay: -1.2s; }
/* =====================================================================
6. ATMOSPHERE (all still, so the flower never seems to change)
===================================================================== */
.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(--t) ease-in-out infinite alternate,
ff-blink var(--b) ease-in-out infinite;
}
.firefly:nth-child(1) { left: 12%; top: 32%; --dx: 9vmin; --dy: -7vmin; --t: 13s; --b: 3.4s; }
.firefly:nth-child(2) { left: 84%; top: 28%; --dx: -8vmin; --dy: 6vmin; --t: 11s; --b: 4.1s; animation-delay: -3s, -1.1s; }
.firefly:nth-child(3) { left: 70%; top: 74%; --dx: 7vmin; --dy: -9vmin; --t: 15s; --b: 2.9s; animation-delay: -6s, -2.2s; }
.firefly:nth-child(4) { left: 24%; top: 70%; --dx: -6vmin; --dy: -5vmin; --t: 12s; --b: 3.7s; animation-delay: -2s, -.4s; }
.firefly:nth-child(5) { left: 46%; top: 12%; --dx: 10vmin; --dy: 4vmin; --t: 17s; --b: 4.6s; animation-delay: -9s, -3s; }
.firefly:nth-child(6) { left: 92%; top: 60%; --dx: -9vmin; --dy: -4vmin; --t: 14s; --b: 3.1s; animation-delay: -4s, -1.7s; }
.firefly:nth-child(7) { left: 6%; top: 86%; --dx: 8vmin; --dy: -6vmin; --t: 16s; --b: 5.2s; animation-delay: -7s, -2.6s; }
.firefly:nth-child(8) { left: 58%; top: 90%; --dx: -7vmin; --dy: -8vmin; --t: 12.5s; --b: 3.9s; animation-delay: -5s, -.9s; }
.moon {
inset: 0; pointer-events: none;
opacity: var(--night-o);
mix-blend-mode: screen;
background: radial-gradient(circle at 6% 4%, rgba(190,215,255,.32), transparent 45%);
}
.light {
inset: 0; pointer-events: none;
mix-blend-mode: soft-light;
background: linear-gradient(135deg, var(--key) 0%, transparent 42%, transparent 60%, var(--fill) 100%);
}
.vignette {
inset: 0; pointer-events: none;
background: radial-gradient(ellipse at 50% 50%, transparent 55%, var(--vig) 100%);
}
/* =====================================================================
7. CONTROL: an outlined button on the right with a swipe fill.
Horizontal by default; a slim vertical tab on phones.
===================================================================== */
.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;
}
/* The swipe: one solid layer sized from 0% to 100%. Its anchor flips
instantly (position isn't transitioned), so it enters from one side
and leaves from the other. The label text carries an identical layer,
clipped to the letters, so the colour change tracks the edge exactly. */
.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);
}
@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; }
}
.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; }
/* Both labels share one grid cell, so the button keeps its size when it swaps */
.when-flying { visibility: hidden; }
.toggle:checked ~ .ui .when-landed { visibility: hidden; }
.toggle:checked ~ .ui .when-flying { visibility: visible; }
/* =====================================================================
8. KEYFRAMES
===================================================================== */
/* Walk half a lap, stop and air the wings, walk the other half, pause */
@keyframes patrol {
0% { --heading: 0deg; animation-timing-function: cubic-bezier(.42, 0, 1, 1); }
3% { --heading: -8deg; animation-timing-function: linear; }
37% { --heading: -172deg; animation-timing-function: cubic-bezier(0, 0, .58, 1); }
40%, 56% { --heading: -180deg; animation-timing-function: cubic-bezier(.42, 0, 1, 1); }
59% { --heading: -188deg; animation-timing-function: linear; }
93% { --heading: -352deg; animation-timing-function: cubic-bezier(0, 0, .58, 1); }
96%, 100% { --heading: -360deg; }
}
@keyframes routine {
0% { --walk: 0; --open: 0; --sense: 0; }
3% { --walk: 1; }
37% { --walk: 1; --sense: 0; }
40% { --walk: 0; --sense: 1; --open: 0; }
43.5% { --open: 1; }
51% { --open: 1; }
55% { --open: 0; --sense: 1; }
56% { --walk: 0; }
59% { --walk: 1; --sense: 0; }
93% { --walk: 1; }
96% { --walk: 0; --sense: 0; }
98% { --sense: 1; }
100% { --walk: 0; --sense: 0; --open: 0; }
}
@keyframes step {
0% { --stride: -1; --fold: 0; animation-timing-function: linear; }
60% { --stride: 1; --fold: 0; animation-timing-function: ease-in-out; }
80% { --fold: 1; }
100% { --stride: -1; --fold: 0; }
}
@keyframes sway { 0%, 100% { --sway: -1; } 50% { --sway: 1; } }
@keyframes bob { from { --bob: -1; } to { --bob: 1; } }
@keyframes flap { from { --flap: -1; } to { --flap: 1; } }
@keyframes twitch {
0%, 100% { --twitch: 0; }
20% { --twitch: 1; }
27% { --twitch: -.4; }
50% { --twitch: .2; }
63% { --twitch: -1; }
70% { --twitch: .5; }
85% { --twitch: 0; }
}
@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) {
.orbit, .beetle, .flight, .leg, .wing, .antenna, .firefly { animation: none !important; }
.beetle { transition-duration: .2s; }
.btn, .btn__label { transition-duration: .01s; }
}
</style>
</head>
<body>
<input type="checkbox" id="fly" class="toggle" aria-label="Startle the ladybird">
<main class="stage" aria-label="A seven-spot ladybird walking around the centre of a daisy">
<div class="lawn"></div>
<div class="flower">
<div class="flower__shadow"></div>
<div class="ring ring--back">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="ring ring--front">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="disc"></div>
</div>
<div class="orbit">
<div class="track">
<label class="beetle" for="fly" title="Startle the ladybird">
<span class="ao"></span>
<span class="flight">
<!-- Lower layer, both sides: legs, abdomen, wing, wing case -->
<span class="half half--l">
<span class="leg leg--1"><span class="femur"><span class="tibia"><span class="tarsus"></span></span></span></span>
<span class="leg leg--2"><span class="femur"><span class="tibia"><span class="tarsus"></span></span></span></span>
<span class="leg leg--3"><span class="femur"><span class="tibia"><span class="tarsus"></span></span></span></span>
<span class="abdomen"></span>
<span class="wing"></span>
<span class="elytron"></span>
</span>
<span class="half half--r">
<span class="leg leg--1"><span class="femur"><span class="tibia"><span class="tarsus"></span></span></span></span>
<span class="leg leg--2"><span class="femur"><span class="tibia"><span class="tarsus"></span></span></span></span>
<span class="leg leg--3"><span class="femur"><span class="tibia"><span class="tarsus"></span></span></span></span>
<span class="abdomen"></span>
<span class="wing"></span>
<span class="elytron"></span>
</span>
<!-- Centre line: head tucked under the shield -->
<span class="head"></span>
<span class="pronotum"></span>
<!-- Upper layer, both sides: eyes and antennae -->
<span class="half half--l">
<span class="eye"></span>
<span class="antenna"></span>
</span>
<span class="half half--r">
<span class="eye"></span>
<span class="antenna"></span>
</span>
</span>
</label>
</div>
</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><span class="firefly"></span><span class="firefly"></span>
</div>
<div class="moon"></div>
<div class="light"></div>
<div class="vignette"></div>
</main>
<div class="ui">
<label class="btn" for="fly">
<span class="btn__label when-landed">Startle it</span>
<span class="btn__label when-flying">Let it land</span>
</label>
</div>
</body>
</html>