How Ephemera was made

Four AI-generated museum plates, sampled into sixty thousand GPU particles each. One geometry, two target states, one crossfade uniform — and a cursor that is legally wind.

01 · The specimens

The exhibit artwork was generated with GPT Image 2 via Higgsfield, prompted as a coherent museum series — "Specimens of Light" — with one hard constraint: a single luminous subject, centered, on a pure black field. The black is not an aesthetic; it is the data format. Particles are sampled by brightness, so anything black simply doesn't exist.

Luna moth plateNo.01 moth
X-ray peony plateNo.02 bloom
Marble hand plateNo.03 relic
Ember chrysanthemum plateNo.04 ember

02 · Image → particles

draw plate into a 240×320 offscreen canvas
for each pixel:  luma = .32r + .55g + .13b
  if luma < 0.085 → skip            // jpeg-noise floor: black is not a specimen
  else → candidate {x, y, color, luma}
fill 60,000 slots from candidates (near-uniform density,
  bright pixels get at most a 2× chance — structure keeps its particles,
  brightness is carried by color and size instead)
z = (luma − .42) · 2.4 + jitter      // bright pixels float forward
color = pow(rgb, 1.6) · gain         // gamma-deepen: darks die, brights glow

03 · One geometry, two futures

Every particle owns two target states: aT0 (where it is) and aT1 (where it's going), plus a seed. Morphing is a single uniform:

m   = smoothstep(clamp(uMix·1.3 − seed·0.3))   // dim particles leave first,
pos = mix(aT0, aT1, m)                          // bright ones arrive last
pos += valueNoise(pos·0.32 + t) · sin(m·π) · 7.5 // mid-morph: the image becomes weather

When the tween lands, aT1 is copied into aT0, uMix resets to zero, and the museum is quietly ready for its next departure. The swarm needs no physics simulation and no FBO ping-pong — it is a pure function of one number, which is why it never breaks.

04 · The two interactions

05 · Art direction

Darkroom brown-black #0D0A07 (never pure black — pure black is where the particles live), bone #EDE3D2, copper #C88A5E for the brass of the museum. Plaques in Cormorant Garamond — small caps energy, italic captions; catalog numbers in Fragment Mono. CSS grain, a faint copper halo behind the exhibit, 420 motes of hall dust for depth. Additive blending does the glowing; there is deliberately no post-processing.

06 · Iteration log (three passes)

Pass 1 — correctness

Headless framebuffer verification (manual frame stepping + readPixels ASCII maps) of all four exhibits, the swarm mid-morph, and the dissolve. Found and fixed: a jpeg-noise particle haze (raised the luma floor), washed-out palette and a gray-lifted background traced to the post-processing chain double-encoding sRGB — solved by deleting the composer entirely; additive sprites already bloom.

Pass 2 — complexify

Per-exhibit gain so the ember's thin filaments speak at the same volume as the moth's wings; near-uniform sampling density to preserve fine structure; hall dust; exhibit-aware page titles; keyboard navigation; auto-advance ring timed at sixteen seconds.

Pass 3 — polish

Mobile audit (gate wordmark tracking overflowed 375px — retracked), plaque/catalog collision check, reduced-motion path, gsap lag-smoothing disabled so hidden tabs resume by completing rather than crawling, meta/OG, console-clean.

07 · Reproduce it

ephemera/
├── index.html          # gate, plaque, catalog, one canvas
├── css/main.css        # museum chrome: plaque, dots, grain, halo
├── js/main.js          # ~430 lines: sampler, shader, morphs, wind
├── assets/*.jpg        # 4 plates, 716×960, <470 KB total
├── lib/                # three.module.js, gsap.min.js
└── fonts/              # Cormorant Garamond + Fragment Mono

# generate your own specimens (any model works):
higgsfield generate create gpt_image_2 --prompt "single luminous subject,
  perfectly centered, PURE BLACK background …" --aspect_ratio 3:4 --wait