hero: one wash for both themes — revert the continuous-blend machinery

Reverts the per-frame color sampling and luminance-blended wash modes:
the right fix was removing the second wash entirely. Cards now dim via
color multiply in both themes (hue-true; the light theme's alpha wash
toward the paper shifted footage pastel, and having two treatments is
what made theme fades show a mode switch at all). watchThemeColors
returns to the simple flip + settle double-derive.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
This commit is contained in:
Tanish Manku committed 2026-09-11 00:10:37 -07:00
1 parent 0083dc140f
commit 7bc1407648
3 files changed
+18 -33

No files matched your search

@@ -453,7 +453,7 @@ export const galaxyLookTuner = defineTunables("galaxyLook", "Galaxy look", {
max: 1,
step: 0.02,
default: 1,
info: "Overall footage strength (dark theme dims via color, light theme washes via opacity).",
info: "Overall footage strength — dims via color multiply, the same in both themes.",
},
aberration: {
label: "Dispersion",
@@ -335,9 +335,9 @@ function GalaxyScene({
boostCard: -1,
});
// NOTE: no boolean dark flag — the light/dark wash treatment blends
// continuously by the LERPED background's luminance in the frame loop,
// so a theme fade never snaps between wash modes.
// One wash for both themes: cards dim via color multiply (hue-true —
// an alpha wash toward the paper shifted footage pastel in light mode,
// and two treatments meant a visible mode switch during theme fades).
// One <video>, one shared texture, and one cover-fit window per clip.
const videos = useMemo(
@@ -807,10 +807,6 @@ function GalaxyScene({
cs.line.lerp(cs.tLine, themeK);
lineMat.color.copy(cs.line);
tickMat.color.copy(cs.line);
// Wash-mode blend: light theme washes cards via opacity, dark theme
// dims via color. Driven continuously by the LERPED background's
// luminance — a binary flag snapped the treatment mid-fade.
const darkK = smoothstep(1.95, 1.05, cs.bg.x + cs.bg.y + cs.bg.z);
// Perf governor: EMA of the real frame time. Sustained drops below the
// FPS floor shed cards (and their decode pressure) quickly; recovery
@@ -1241,12 +1237,8 @@ function GalaxyScene({
u.uClickW.value = pt.clickWidth * Math.max(W, H);
u.uRadius.value = Math.min(lk.cornerPx, H * 0.49);
u.uFrameA.value = lk.frameAlpha;
const dimDark = lk.dim * solid + (1 - lk.dim * solid) * ck;
const wash = solid * Math.sqrt(lk.dim);
const alphaLight = lifecycle * waveK * (wash + (1 - wash) * ck);
const alphaDark = lifecycle * waveK;
u.uDim.value = 1 + (dimDark - 1) * darkK;
u.uAlpha.value = alphaLight + (alphaDark - alphaLight) * darkK;
u.uDim.value = lk.dim * solid + (1 - lk.dim * solid) * ck;
u.uAlpha.value = lifecycle * waveK;
// A targeted card's clip outranks everything in the decode budget so
// it plays immediately, wherever it is on the journey.
@@ -67,35 +67,28 @@ export function watchThemeColors(
onChange: (colors: ThemeColors) => void,
): () => void {
const derive = () => onChange(deriveThemeColors());
// Theme flips animate the tokens over 0.4s (see globals.css). Sampling
// just the endpoints left WebGL consumers chasing a stale target until
// the fade ended (a visible late shift); instead, sample the computed
// colors EVERY FRAME through the transition window so canvas layers
// track the DOM's animated tokens exactly.
let raf = 0;
let until = 0;
const sampleLoop = () => {
// Theme flips animate the tokens over 0.4s (see globals.css), so a
// derive at flip time reads MID-transition values. Deliver both: the
// immediate read (consumers that lerp start moving at once) and a final
// read after the fade settles so targets land on the true colors.
let settleTimer: ReturnType<typeof setTimeout> | undefined;
const deriveTwice = () => {
derive();
if (performance.now() < until) raf = requestAnimationFrame(sampleLoop);
};
const onFlip = () => {
until = performance.now() + 550;
cancelAnimationFrame(raf);
derive();
raf = requestAnimationFrame(sampleLoop);
clearTimeout(settleTimer);
settleTimer = setTimeout(derive, 500);
};
derive();
const observer = new MutationObserver(onFlip);
const observer = new MutationObserver(deriveTwice);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
const mq = window.matchMedia("(prefers-color-scheme: dark)");
mq.addEventListener("change", onFlip);
mq.addEventListener("change", deriveTwice);
return () => {
cancelAnimationFrame(raf);
clearTimeout(settleTimer);
observer.disconnect();
mq.removeEventListener("change", onFlip);
mq.removeEventListener("change", deriveTwice);
};
}