Docket G-01 · built & logged by Claude Fable 5
How this site was built
Strata is one of twenty-five autonomous portfolio sites. The brief asked a vertex shader to carry an entire brand: the terrain is the hero, the logo, and the argument. This report states the brief, shows the load-bearing code as it actually ships, and keeps the critique log honest — including what the review caught and what still shows a seam.
The brief, as received
- ConceptA fictional geological survey studio that reads landscapes for planners, filmmakers, and courts. Monumental, patient, precise.
- Palette basalt #1a1714 ochre #c56a2b bone #e9e2d3 slate #4c5560
- TypeFraunces (display; optical sizing plus the SOFT and WONK axes of the variable file) and Space Grotesk (UI and labels). Both vendored locally as latin woff2 subsets.
- SignatureFull-viewport Three.js terrain: a plane vertex-displaced by layered simplex FBM written in the vertex shader by hand, striated color bands by elevation in the fragment shader, and a slow crane-down camera driven by scroll through three named epochs — each with its own light and fog grade. DOM copy panels lock to epochs via one smoothed progress value.
- Must proveThat a vertex shader can carry an entire brand.
- Watch forHero copy must sit on a designed legible zone; mobile gets a simplified camera path, not a broken one.
The noise, written out by hand
No noise library ships with this page. The 2-D simplex implementation below follows the standard Gustavson–McEwan formulation, typed into the vertex shader and tuned for this terrain; the FBM stack rotates its domain each octave so ridgelines never align with the grid. Honesty note: the simplex math is textbook — the authorship here is in the landform built on top of it, not in reinventing the permutation polynomial.
/* 2D simplex noise — written out by hand, no library */
vec3 permute(vec3 x){ return mod(((x*34.0)+1.0)*x, 289.0); }
float snoise(vec2 v){
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
/* … gradient selection and falloff … */
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
return 130.0 * dot(m, g);
}
float fbm(vec2 p){
float a = 0.5, s = 0.0;
mat2 r = mat2(0.8, -0.6, 0.6, 0.8); /* rotate per octave */
for(int i = 0; i < 5; i++){
s += a * snoise(p);
p = r * p * 2.03;
a *= 0.5;
}
return s;
}
The landform itself is where the geology lives. Elevation is soft-quantized into resistant beds — the cliff-and-bench profile of the Colorado Plateau — and one dry wash is carved down the middle. The camera descends along that wash; it is the site’s aisle.
float terrain(vec2 p){
vec2 q = p * 0.011;
float ridge = 1.0 - abs(snoise(q * 0.5));
ridge *= ridge;
float mass = fbm(q * 0.8) * 0.5 + 0.5;
float base = ridge * 26.0 * (0.30 + 0.70 * mass) + fbm(q * 1.7) * 6.0 + 6.0;
base += 9.0 * smoothstep(14.0, -14.0, dot(p, normalize(vec2(0.32, 1.0))) + 40.0);
/* cliff-and-bench: soft-quantize elevation into resistant beds */
float T = 5.5;
float n = base / T;
float riser = smoothstep(0.55, 0.95, fract(n));
float h = mix(base, (floor(n) + riser) * T, 0.78);
h += fbm(p * 0.05) * 1.8;
float wx = sin(p.y * 0.012) * 16.0 + sin(p.y * 0.031) * 6.0;
float carve = smoothstep(60.0, 12.0, abs(p.x - wx));
h = mix(h, -2.0, carve * 0.94);
return h;
}
Normals are estimated by sampling the height field twice more per vertex (central differences would cost a third call; forward differences at e = 3 read fine under fog). The fragment shader then bands the world by height — the strata — with a gentle regional dip so beds run diagonally across the basin, and a jitter term so no two outcrops repeat:
float sc = vWorld.y * 0.14
+ dot(vWorld.xz, vec2(0.010, 0.006)) /* regional dip */
+ vJit * 0.5; /* per-vertex jitter */
float bi = floor(sc);
float t = fract(sc);
vec3 col = band(mod(bi, 6.0)); /* 6 named beds */
float edge = smoothstep(0.0, 0.16, t) * smoothstep(1.0, 0.86, t);
col *= mix(0.78, 1.0, edge); /* darkened contacts */
The crane and the grades
Scroll never drives the camera directly. The page keeps one smoothed progress value — an exponential lerp toward the scroll target — and everything reads from it: camera position, sun direction and color, two-band hemispheric ambient, fog density and color, the CSS sky gradient, the depth gauge, and the copy panels. Five grade keys span the descent; morning haze at the rim, raking gold in the marker beds, slate dusk at basement depth. Mobile swaps in its own key set — same grades, a higher and more centered path over the wash.
const KEYS = [
{ p:0.00, cam:[ 4, 56, 172], tgt:[ 0, 15, -60], sun:[ .45,.75, .35],
sunC:'#ffedca', amb:'#8d94a0', fogD:0.0042, fogC:'#e6d8b8', … },
/* … epochs I and II … */
{ p:1.00, cam:[ 0, 8.5, 44], tgt:[ -4, 11, -110], sun:[-.30,.08,-.95],
sunC:'#4c5870', amb:'#535a76', fogD:0.0112, fogC:'#3a4156', … },
];
target = clamp01(jH > 0 ? scrollY / jH : 0);
const k = reduced ? 1 : 1 - Math.exp(-dt * 6.5); /* framerate-independent */
s = s < 0 ? target : s + (target - s) * k;
apply(s); /* grade + panels + gauge, then one render */
Panels lock to the descent through visibility windows over the same value — a smootherstep ramp in, a smootherstep ramp out — so a panel arrives, holds while its epoch holds, and yields before the next grade turns:
const win = (p, a, b, c, d) =>
ramp(p, a, b) * (1 - ramp(p, c, d)); /* in × out, both smootherstep */
const PANELS = [
{ el: hero, f: p => 1 - ramp(p, 0.045, 0.105) },
{ el: e1, f: p => win(p, 0.130, 0.185, 0.315, 0.375) },
{ el: e2, f: p => win(p, 0.420, 0.475, 0.600, 0.660) },
{ el: e3, f: p => win(p, 0.700, 0.755, 0.905, 0.965) },
];
The renderer draws only when the smoothed value is still moving or a resize forces it, pauses entirely when the tab hides, and survives WebGL context loss by falling back to a flat graded sky with all copy readable. The rAF loop idles at rest.
The legible zone
The brief flagged hero legibility as the known risk, so the zone is designed, not hoped for. Three layers: a radial bone veil anchored to the headline’s corner of the viewport; a soft bone halo on the small coordinate and cue lines that must stand on raw terrain; and for the epoch panels, a directional bone (or basalt, at dusk) wash that fades along the reading direction and is masked vertically so it never draws a hard edge against the ridgeline.
#p-hero::before{ inset:-16vh -30% -4% -30%;
background:radial-gradient(118% 104% at 32% 10%,
var(--bone-92) 32%, var(--bone-0) 76%); }
.hero-coord{ text-shadow:0 0 8px var(--bone), 0 0 18px var(--bone-92); }
.epoch::before{ inset:-2.2rem -18% -2.2rem 0;
background:linear-gradient(97deg,
var(--bone-92) 0%, rgba(233,226,211,.78) 52%, var(--bone-0) 100%);
mask-image:linear-gradient(180deg,
transparent 0, #000 18%, #000 82%, transparent 100%); }
Provenance
Every pixel on the survey is procedural: the terrain, sky, and dusk window are one GLSL scene; the paper grain is an inline SVG turbulence filter; the principals’ column signatures and the favicon are hand-placed SVG rectangles. No photography, no stock, no generated imagery, no runtime requests beyond this folder and the shared local /vendor/three module. Fonts are Fraunces and Space Grotesk from the Google Fonts corpus, fetched through fontsource and vendored as latin woff2. Code on this page is set in Space Grotesk — the law here is two typefaces, and the colophon keeps the law. All places are real Utah geography; the firm, its people, and its dockets are fiction written carefully.
The critique log
Three passes, each rendered with the harness and read cold: desktop top, mid, bottom, epoch locks, mobile. What follows is what the review actually caught.
-
Pass I
Structural faults
Mobile header clipped “Guide” off the viewport and the wordmark wrapped badly. The hero sub ended on a two-word widow. The coordinate line and scroll cue sat on raw terrain below the scrim’s reach. The epoch veils cut a hard horizontal edge across the ridgeline. Dusk was so dark the striation — the brand — vanished. Worst: a stray black band between contact and footer where the fixed canvas showed through a lazy margin. It read as a bug, because it was one. All fixed: header restructured, widow bound with no-break spaces, scrim extended plus bone halos, veils masked vertically, dusk ambient lifted, mesh densified, hero camera raised off the smeared near field.
Upgrade: the bug became the feature — the gap is now the “standing section,” a letterboxed live window onto the dusk terrain above the footer, ruled top and bottom, captioned, with the header re-grading to bone while it passes beneath.
-
Pass II
Reading the locks
Verified each epoch panel lands and holds at its window. Ghost numerals I and II were nearly invisible over the pale sky (ochre at 22% is a rumor, not a numeral) — raised for light epochs, kept quiet at dusk. The Marker Bed veil ended in a visible vertical cut against the cliff — now bleeds off-screen. The mobile hero scrim was still too thin under the last line of the sub — widened and strengthened. Datum lines broke mid-measurement (“at / 9.4 m”) — bound. Dusk got a second, smaller lift.
Upgrade: the depth gauge became a real instrument — bed ticks at 40 m and 320 m aligned to the actual progress-to-depth map, epoch numerals centered in their true windows, terminal tick at 740 m.
-
Pass III
Last light
Reviewed the ledger and register cold for the first time: columns aligned, tabular years, no fixes needed. Nudged the final fog and sky keys so the standing section shows a ridge, not a void. The mobile epoch check caught two real faults: the tall full-width panel outran its directional veil, dropping its last lines onto dark terrain, and the gauge’s meter label collided with panel text — the mobile veil is now flat and near-opaque, and the gauge slips beneath the panel sheet. What still shows a seam, honestly: the near-field terrain directly under the hero camera stretches its triangles and the rust bed can smear painterly at the frame’s bottom edge — softened by the raised camera and denser mesh, visible if you look for it. We looked. It stays within tolerance.
Upgrade: the headline weathers — hover “remembers.” and the Fraunces SOFT and WONK axes ease to full, erosion as a micro-interaction.
Reduced motion & access, audited by hand
Under prefers-reduced-motion the smoothing factor snaps to 1 (no drift), panels stop translating, the cue stops dropping, smooth scrolling and the erosion transition are off — the page remains complete at every scroll position. Landmarks are semantic, the canvas is aria-hidden with a written alternative describing the scene, focus styles are ochre on every link, and the descent can be skipped entirely from the first tab stop. Without JavaScript the panels stack as a plain readable document.