Build notes · Claude Fable 5 + Claude Opus 4.8

How Irida was built

One HTML file, one shader, zero image assets. These are the real notes: the concept, the techniques, and the three iteration passes that got it to ship.

01

Concept

The fiction. Irida is an AI observability platform. It traces every prompt, tool call, and token an LLM stack produces, then shows latency, cost, and eval quality live. The name comes from Iris, the Greek rainbow goddess. Observability tools promise to show you light you can't normally see, so the whole identity hangs on one image: white light hitting black glass and splitting into spectrum.

The art direction. Obsidian laboratory. The field is near-black (#0A0A0F), surfaces are barely lighter (#12121A), and the three iridescent accents (#FF6EC7, #00F0FF, #8B5CF6) never appear as flat fills. They only exist as light: gradients, shader fresnel, glows, and gradient-clipped text. That restraint is what keeps a three-accent palette from turning into a carnival. Space Grotesk carries display and UI because its slightly squared curves read technical without going full sci-fi. IBM Plex Mono handles every number and label, since an observability product is mostly numbers wearing a nice coat.

The money shot. A floating blob of black glass, displaced by simplex noise, lit by nothing but its own iridescent fresnel rim. It rotates slowly, leans toward your pointer, and ripples when you click. The headline sits right on top of it. Everything else on the page (trace stream, waterfall, pricing) is deliberately quieter so the hero stays the loudest thing in the room.

02

Technique breakdown

Fresnel iridescence shader GLSL

The blob's color is not textured. A fresnel term measures how edge-on each fragment is to the camera, then indexes a three-stop spectral ramp (cyan, violet, pink) that wraps like thin-film interference. The base stays near-black so the rim reads as light on glass.

// fragment: rim light picks the hue, base stays obsidian
float fres = pow(1.0 - ndv, 2.3);
float band = N.x*0.22 + N.y*0.34 + vNoise*1.4
           + fres*0.85 + uTime*0.016;
vec3 film = iri(band);   // cyan -> violet -> pink wrap
vec3 col  = base
          + film * fres * 1.25
          + PINK * spec1 * 0.85
          + CYAN * spec2 * 0.55;

Noise displacement with true normals GLSL

An icosahedron gets displaced along its normals by 3-octave simplex fbm. Displacing vertices breaks their normals, so the vertex shader displaces two tangent neighbors too and rebuilds the normal from the cross product. Without this step the lighting stays glued to the old sphere and the blob looks like a balloon.

vec3 pos = displace(position);
vec3 p1 = displace(normalize(position + tang * e) * r);
vec3 p2 = displace(normalize(position + bitang * e) * r);
vec3 nn = normalize(cross(p1 - pos, p2 - pos));

Pointer physics, not pointer snapping JS

The mouse never drives the scene directly. A target position updates on pointermove and the working value eases toward it with frame-rate independent damping. The same trick decays the click ripple. Everything the pointer touches feels like it has mass.

// frame-rate independent lerp: same feel at 30 or 144hz
const k = 1 - Math.pow(0.94, dt * 60);
mouse.x += (mouse.tx - mouse.x) * k;
uniforms.uKick.value *= Math.pow(0.94, dt * 60);
group.rotation.y = spin + mouse.x * 0.34;

Live trace stream JS

The terminal in the features grid generates plausible spans (op, model, status, latency, cost) on a randomized 380 to 900ms clock, appends them as syntax-colored rows, and prunes the top. An IntersectionObserver pauses the generator when the card scrolls away, so it costs nothing off-screen.

function tick(){
  makeLine();
  timer = setTimeout(tick, rnd(380, 900));
}
new IntersectionObserver(([e]) => {
  e.isIntersecting ? tick() : clearTimeout(timer);
}).observe(body);

Reveal choreography CSS + JS

Hero text enters blur-to-sharp with 80ms staggers under a 1.2s budget. Scroll content uses one IntersectionObserver that adds a class once and unobserves. Waterfall bars, spark lines, and the eval ring all animate off that same class, each with its own transition delay, so a section lands as one choreographed beat.

.hr{opacity:0;filter:blur(14px);transform:translateY(22px);
  animation:hr-in 1s var(--ease-out) forwards}
.wf-bar{transform:scaleX(0);transform-origin:left;
  transition-delay:calc(var(--i) * 90ms)}
.in .wf-bar{transform:scaleX(1)}
03

Asset pipeline

Everything is procedural. There are zero image files. The blob and its particle field are three.js (pinned 0.160.0 from jsdelivr) with hand-written GLSL. The film grain is an inline SVG feTurbulence data URI on a fixed overlay. The favicon is an inline SVG data URI of the prism mark. Icons are hand-drawn inline SVG strokes sharing two global gradient defs. The customer logos are typographic marks. The dashboard, waterfall, diff view, and terminal are plain HTML and CSS.

Total payload lands around 40KB of HTML plus fonts and the three.js module. Nothing else ships.

04

Recreate it

Paste this into Claude and adjust the fiction to taste.

Role: You're an art director and creative developer who ships
hand-written HTML/CSS/JS with three.js shaders.

Task: Build a one-page marketing site for a fictional AI
observability product, plus a /guide page of build notes.

Context: Mood is "obsidian laboratory, iridescent light on black
glass." Background #0A0A0F, surface #12121A, text #EDEDF2, muted
#8A8A9E. Accents #FF6EC7 / #00F0FF / #8B5CF6 appear only as
gradients or shader light, never flat fills. Type: Space Grotesk
for display and UI, IBM Plex Mono for labels and data.

Format: Static files only. index.html with inline CSS/JS. three.js
0.160.0 from cdn.jsdelivr.net as the only library. Works from
file:// and https://.

Constraints:
- Hero: full-viewport icosahedron displaced by 3-octave simplex
  noise in the vertex shader, normals rebuilt from displaced
  tangent neighbors, fresnel-driven 3-stop iridescent ramp in the
  fragment shader. Headline overlaps the canvas. Pointer eases the
  rotation with damped lerp. Click adds a decaying ripple.
- Feature grid with one live demo: a fake trace stream appending
  syntax-colored log rows on a randomized timer.
- A trace waterfall mock, 3 pricing tiers, full footer.
- IntersectionObserver reveals (once, staggered). Custom
  cubic-beziers. prefers-reduced-motion collapses all motion.
- Copy: contractions, no em-dashes, no buzzwords, no testimonials.

Examples: fresnel = pow(1.0 - dot(N, V), 2.3); ramp cyan to violet
to pink; damping k = 1 - pow(0.94, dt * 60).
05

Attribution

Two models worked on this page, so here's who did what.

Claude Fable 5 did the design and the build: the concept, the art direction, the shaders, the copy, the markup, and iteration passes 1 and 2. Partway through, Fable 5 ran out of usage credits and stopped mid-project.

Claude Opus 4.8 picked it up from there and finished it: iteration pass 3 (mobile QA, guide route, accessibility and reduced-motion checks), the deploy, and the live verification. The design is Fable's. The last mile is Opus's.

06

Iteration log

  • Pass 1 · Structure · Fable 5
    • Blob read as coral, not glass. Dropped noise frequency from 1.65 to 0.92 and raised amplitude so it rolls in big smooth lobes, tightened the fresnel exponent to pull the iridescence back to the rim.
    • The glow shell rendered as a hard sphere outline around the blob. Gave the halo the same displacement, scaled it from 1.32 down to 1.09, and cut its alpha so it hugs the surface.
    • Hero subline and kicker drowned against bright shader areas. Added a radial scrim behind the hero text block, text shadows, and brighter kicker color.
    • Waterfall duration labels sat on top of the bars. Moved them out of the track into their own 66px grid column.
    • Customer marks at #5E5E72 were nearly invisible in full-page shots. Raised to #84849C.
    • Particles rendered as hard squares (PointsMaterial default). Swapped in a radial-gradient canvas sprite.
    • CSS scroll-behavior:smooth made programmatic scrolls animate, which smeared full-page captures. Moved smooth scrolling into a JS click handler for anchors only.
    • Showcase lede was meant to sit beside the heading and wrapped under it instead. Constrained both columns.
  • Pass 2 · Depth · Fable 5
    • Added two thin iridescent orbit rings around the blob (vertex-colored torus meshes, additive blending) with their own pointer parallax. The hero now reads as a specimen inside an instrument.
    • Rebuilt the trace stream as a real console: fixed column widths in ch units, a column header row, and a pink alert row that interrupts the flow every eleventh line.
    • Gave the showcase panel working Waterfall and Raw tabs. Raw shows the same trace as syntax-colored JSON.
    • Added hover states to waterfall rows (row tint, name brightens, bar glows) and a shine sweep across primary buttons.
    • Gave every fictional customer mark its own small SVG glyph so the logo wall stops being six text strings.
    • Added quota meter bars under each pricing tier, a scanning light streak on section separators, and a pulsing status pill in the footer.
  • Pass 3 · Final QA · Opus 4.8
    • This guide page scrolled sideways 95px at 390px. Wide <pre> blocks were stretching their grid tracks, since grid items default to min-width:auto. Added min-width:0 guards to the card grid, cards, and prompt block.
    • Tap targets were under 44px on mobile: the nav CTA, footer links, and panel tabs. Set min-heights on all three.
    • Strengthened the hero text scrim at narrow widths, where the blob sits directly behind the subline.
    • Swapped the footer credit to name both models and added the attribution section above, since Fable 5 ran out of credits mid-project.
    • Verified prefers-reduced-motion: the WebGL loop renders two static frames instead of animating, and every reveal, meter, ring, and streak resolves to its final state.
    • Confirmed both routes ship clean consoles, no horizontal overflow at 390px or 1440px, favicon, title, meta description, and og tags on both pages, then deployed and re-shot the live URL to check parity.