ruler+hero: pools under everything & occupancy-gated; scattered spawn order

The contrast pools move to their own z-39 layer, so the hero wordmark's
z-40 letters (and everything else — rail, section words) draw above
them; their masks now complete the fade inside the pool's own bounds so
the rectangle edge never reads as a cut. Pool presence follows real
occupancy, driven from the frame loop: the top pool fades in as the
wordmark settles into the stack, the bottom pool fades out as the last
queued word climbs onto the rail.

The rail frost's inner feather becomes a generated power-curve gradient
easing in toward the outer edge, with Frost solid % and Frost ease
knobs. And the galaxy's aligned-ring spawn is fixed: card index order
is arm-aligned (round-robin deal), so the intro stagger and the
governor's shed/regrow now walk a golden-ratio permutation instead —
plus a 4s governor grace period over the load stutter, which was
shedding the intro and regrowing it as visible rings.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
This commit is contained in:
Tanish Manku committed 2026-09-10 13:56:28 -07:00
1 parent 81ee9a3533
commit dfbc443c23
4 files changed
+136 -49

No files matched your search

@@ -453,6 +453,22 @@ function GalaxyScene({
})),
[layout],
);
// Index order is arm-aligned (arms deal round-robin), so anything that
// reveals cards sequentially by index — the intro stagger, the governor
// regrowing shed cards — would paint aligned rings. This golden-ratio
// permutation scatters that order across arms and slots.
const liveOrder = useMemo(() => {
const idx = cards.map((_, i) => i);
idx.sort((a, b) => ((a * 0.618034) % 1) - ((b * 0.618034) % 1));
return idx;
}, [cards]);
const liveRank = useMemo(() => {
const r = new Int32Array(cards.length);
liveOrder.forEach((ci, k) => {
r[ci] = k;
});
return r;
}, [cards, liveOrder]);
const materials = useMemo(
() =>
cards.map(
@@ -577,24 +593,28 @@ function GalaxyScene({
clipBest.fill(-1);
// Perf governor: EMA of the real frame time. Sustained drops below the
// FPS floor shed the highest-index cards (and their decode pressure)
// quickly; recovery regrows slowly so it never oscillates.
// FPS floor shed cards (and their decode pressure) quickly; recovery
// regrows slowly so it never oscillates. The first seconds are a grace
// period — page load always stutters (shader compile, video priming)
// and shedding the intro looks like a broken spawn.
st.frameEma += (Math.min(delta, 0.25) - st.frameEma) * 0.05;
if (mv.perfFloor > 0) {
if (mv.perfFloor > 0 && t > 4) {
const budget = 1 / mv.perfFloor;
if (st.frameEma > budget) {
st.perfScale = Math.max(0.35, st.perfScale - dt * 0.3);
} else if (st.frameEma < budget * 0.7) {
st.perfScale = Math.min(1, st.perfScale + dt * 0.05);
}
} else {
} else if (mv.perfFloor === 0) {
st.perfScale = 1;
}
const liveN = Math.max(6, Math.round(cards.length * st.perfScale));
// Pass 1: place every live card along its arm. All positions must be
// known before any card can size itself against its neighbors.
for (let i = 0; i < liveN; i++) {
// Pass 1: place every live card along its arm (in permuted order — the
// live set is the first liveN entries of liveOrder). All positions must
// be known before any card can size itself against its neighbors.
for (let k = 0; k < liveN; k++) {
const i = liveOrder[k];
const card = cards[i];
const c = cycle(
(card.slot + 0.5) / L.slotsPerArm + P + card.arm * L.armJitter,
@@ -618,7 +638,7 @@ function GalaxyScene({
for (let i = 0; i < cards.length; i++) {
const mesh = cardRefs.current[i];
if (!mesh) continue;
if (i >= liveN) {
if (liveRank[i] >= liveN) {
mesh.visible = false;
continue;
}
@@ -641,7 +661,8 @@ function GalaxyScene({
// is eased so freed space fills organically; shrinking is instant so
// the guarantee never breaks mid-frame.
let sep = Infinity;
for (let j = 0; j < liveN; j++) {
for (let k = 0; k < liveN; k++) {
const j = liveOrder[k];
if (j === i) continue;
const s = Math.max(
(Math.abs(cardPos[j * 2] - x) * 9) / 16,
@@ -673,7 +694,8 @@ function GalaxyScene({
const lifecycle = clamp01(c / lk.fadeBand);
const solid = lk.washInner + (1 - lk.washInner) * c;
const intro = clamp01(
(t - mv.introDelay - i * mv.introStagger) / Math.max(0.05, mv.introDur),
(t - mv.introDelay - liveRank[i] * mv.introStagger) /
Math.max(0.05, mv.introDur),
);
const ready = videos[card.clip].readyState >= 2 ? 1 : 0;
@@ -91,6 +91,8 @@ export default function HeadingFlow({
null,
);
const wordRefs = useRef<WordRefs[]>([]);
const topPoolRef = useRef<HTMLDivElement>(null);
const bottomPoolRef = useRef<HTMLDivElement>(null);
const fs = useRef({
lastY: 0,
vel: 0,
@@ -240,6 +242,26 @@ export default function HeadingFlow({
else if (p.flipP > 0) flipShift += p.flipP;
}
// Pool presence follows real occupancy: the top pool fades in as the
// first word (the wordmark) settles into the stack; the bottom pool
// fades out as the last queued word climbs onto the rail. The hero
// never holds the bottom pool — it has no queue home.
let topK = 0;
let botK = 0;
for (let wi = 0; wi < phases.length; wi++) {
if (phases[wi].flipP > topK) topK = phases[wi].flipP;
if (!sections[wi].isHero) {
const q = 1 - phases[wi].detachP;
if (q > botK) botK = q;
}
}
if (topPoolRef.current) {
topPoolRef.current.style.opacity = String(lk.poolAlpha * topK);
}
if (bottomPoolRef.current) {
bottomPoolRef.current.style.opacity = String(lk.poolAlpha * botK);
}
// Horizontal home for a word: letters run inward from the rail, with
// the reading direction arranged so the word's tail sits nearest the
// rail on a right-side ruler (and its head nearest on the left).
@@ -594,46 +616,54 @@ export default function HeadingFlow({
layout.headingPx +
pools.poolPad;
const bottomPoolH = layout.queuePad + n * layout.queueSlot + pools.poolPad;
// Masks complete their fade INSIDE the pool's box (transparent by 97% of
// the box, not of an oversized ellipse), so the rectangle bounds never
// read as a cut.
const poolMask = (cornerY: string) =>
`radial-gradient(115% 115% at ${side === "right" ? "100%" : "0%"} ${cornerY}, black 45%, rgba(0,0,0,0.8) 62%, rgba(0,0,0,0.45) 78%, rgba(0,0,0,0.15) 91%, transparent 100%)`;
`radial-gradient(100% 100% at ${side === "right" ? "100%" : "0%"} ${cornerY}, black 38%, rgba(0,0,0,0.82) 56%, rgba(0,0,0,0.48) 72%, rgba(0,0,0,0.2) 86%, rgba(0,0,0,0.05) 94%, transparent 97%)`;
return (
<div className="pointer-events-none fixed inset-0 z-40">
<>
{/* Contrast pools: frosted page-bg fades pinned to the rail's top and
bottom corners, so the top stack and the bottom queue always read
over whatever content scrolls beneath them. Blur level matches the
rail frost, so a letter flying between rail and stack stays in one
continuous frosted world. */}
<div
aria-hidden
className="absolute top-0"
style={{
...(side === "right" ? { right: 0 } : { left: 0 }),
width: poolW,
height: topPoolH,
opacity: pools.poolAlpha,
backdropFilter: "blur(9px)",
WebkitBackdropFilter: "blur(9px)",
backgroundColor: "color-mix(in srgb, var(--bg) 80%, transparent)",
maskImage: poolMask("0%"),
WebkitMaskImage: poolMask("0%"),
}}
/>
<div
aria-hidden
className="absolute bottom-0"
style={{
...(side === "right" ? { right: 0 } : { left: 0 }),
width: poolW,
height: bottomPoolH,
opacity: pools.poolAlpha,
backdropFilter: "blur(9px)",
WebkitBackdropFilter: "blur(9px)",
backgroundColor: "color-mix(in srgb, var(--bg) 80%, transparent)",
maskImage: poolMask("100%"),
WebkitMaskImage: poolMask("100%"),
}}
/>
continuous frosted world. Below z-40: everything — rail, section
letters, AND the hero wordmark's z-40 spans — draws above them.
Opacity is occupancy-driven from the frame loop (starts empty). */}
<div aria-hidden className="pointer-events-none fixed inset-0 z-[39]">
<div
ref={topPoolRef}
className="absolute top-0"
style={{
...(side === "right" ? { right: 0 } : { left: 0 }),
width: poolW,
height: topPoolH,
opacity: 0,
backdropFilter: "blur(9px)",
WebkitBackdropFilter: "blur(9px)",
backgroundColor: "color-mix(in srgb, var(--bg) 80%, transparent)",
maskImage: poolMask("0%"),
WebkitMaskImage: poolMask("0%"),
}}
/>
<div
ref={bottomPoolRef}
className="absolute bottom-0"
style={{
...(side === "right" ? { right: 0 } : { left: 0 }),
width: poolW,
height: bottomPoolH,
opacity: 0,
backdropFilter: "blur(9px)",
WebkitBackdropFilter: "blur(9px)",
backgroundColor: "color-mix(in srgb, var(--bg) 80%, transparent)",
maskImage: poolMask("100%"),
WebkitMaskImage: poolMask("100%"),
}}
/>
</div>
<div className="pointer-events-none fixed inset-0 z-40">
{sections.map((s, wi) => {
const refs = (wordRefs.current[wi] ??= {
letters: [],
@@ -675,7 +705,8 @@ export default function HeadingFlow({
</Fragment>
);
})}
</div>
</div>
</>
);
}
@@ -363,6 +363,22 @@ export const rulerLookTuner = defineTunables("rulerLook", "Ruler look", {
default: 1,
info: "Strength of the page-bg contrast pools behind the top stack and the bottom queue (0 = none).",
},
frostSolid: {
label: "Frost solid %",
min: 0,
max: 80,
step: 5,
default: 40,
info: "Portion of the rail (from the outer edge) where the frost is at full strength before the feather begins.",
},
frostGamma: {
label: "Frost ease",
min: 0.5,
max: 4,
step: 0.1,
default: 2.2,
info: "Falloff curve of the frost's inner feather — higher eases in longer toward the page (a slower, less visible tail).",
},
poolPad: {
label: "Pool pad px",
min: 0,
@@ -550,9 +550,9 @@ export default function ScrollRuler() {
>
{/* Frost underlay: content flows under the rail (no reserved
gutter); this pane blurs and tints whatever passes beneath so
the instrumentation always reads. The inner edge feathers on a
smoothstep-like curve — soft enough that letters flying in and
out never read a seam. */}
the instrumentation always reads. The inner feather follows a
tunable power curve easing in toward the outer edge, so no seam
reads while letters fly in and out. */}
<div
aria-hidden
className="absolute inset-0"
@@ -560,8 +560,8 @@ export default function ScrollRuler() {
backdropFilter: "blur(9px)",
WebkitBackdropFilter: "blur(9px)",
backgroundColor: "color-mix(in srgb, var(--bg) 55%, transparent)",
maskImage: `linear-gradient(${side === "right" ? "to left" : "to right"}, black 50%, rgba(0,0,0,0.86) 64%, rgba(0,0,0,0.6) 76%, rgba(0,0,0,0.32) 86%, rgba(0,0,0,0.1) 94%, transparent 100%)`,
WebkitMaskImage: `linear-gradient(${side === "right" ? "to left" : "to right"}, black 50%, rgba(0,0,0,0.86) 64%, rgba(0,0,0,0.6) 76%, rgba(0,0,0,0.32) 86%, rgba(0,0,0,0.1) 94%, transparent 100%)`,
maskImage: frostMask(side, look.frostSolid, look.frostGamma),
WebkitMaskImage: frostMask(side, look.frostSolid, look.frostGamma),
}}
/>
@@ -720,3 +720,21 @@ export default function ScrollRuler() {
</>
);
}
// The frost's inner feather: full strength for the first `solid` percent
// from the outer edge, then a power-curve falloff — alpha = (1 - x)^gamma
// over the remaining span, sampled into gradient stops. Higher gamma spends
// its fade early and approaches the page on a long, invisible tail.
function frostMask(side: RulerSide, solid: number, gamma: number): string {
const dir = side === "right" ? "to left" : "to right";
const stops = [`black ${solid.toFixed(0)}%`];
const steps = 8;
for (let k = 1; k < steps; k++) {
const x = k / steps;
const a = Math.pow(1 - x, gamma);
const pos = solid + x * (100 - solid);
stops.push(`rgba(0,0,0,${a.toFixed(3)}) ${pos.toFixed(1)}%`);
}
stops.push("transparent 100%");
return `linear-gradient(${dir}, ${stops.join(", ")})`;
}