Baseline: full tree snapshot after git repository loss

The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.
This commit is contained in:
KeelrLeek committed 2026-07-22 17:57:00 -05:00
commit a667b4b28e
161 files changed
+53170

No files matched your search

+255
View File
@@ -0,0 +1,255 @@
<script>
import { onDestroy, onMount } from "svelte";
import { getCatalogHeroGpuSource } from "../lib/state.svelte.js";
import { createHeroGpuRenderer } from "../lib/heroGpu.js";
let {
source = "",
scope = "",
plan = null,
active = true,
onloading = () => {},
onready = () => {},
onerror = () => {},
} = $props();
let canvas;
let bloomCanvas;
let previousCanvas;
let previousBloomCanvas;
let renderer = null;
let rendererError = null;
let rendererReady = $state(false);
let painted = $state(false);
let previousVisible = $state(false);
let previousFading = $state(false);
let generation = 0;
let transitionTimer;
let resizeObserver;
let renderedScope = null;
function reportFailure(error, failedSource = source) {
if (!failedSource) return;
onerror(failedSource, error instanceof Error ? error : new Error(String(error)));
}
onMount(() => {
try {
renderer = createHeroGpuRenderer(
canvas,
bloomCanvas,
previousCanvas,
previousBloomCanvas,
(error) => reportFailure(error),
);
resizeObserver = new ResizeObserver(() => renderer?.resize());
resizeObserver.observe(canvas);
rendererReady = true;
} catch (error) {
// Initialization often overlaps the art search, before `source`
// exists. Mark it settled so the first real source can trigger the
// normal fallback path instead of remaining on an empty canvas.
rendererError = error instanceof Error ? error : new Error(String(error));
rendererReady = true;
}
});
$effect(() => {
const selected = source;
const selectedScope = scope;
const cropPlan = plan;
const visible = active;
const ready = rendererReady;
// Start payload preparation even while WebGL programs are compiling.
// The keyed state cache makes the renderer-ready rerun share this work.
const payloadReady = selected && visible && (!ready || renderer)
? getCatalogHeroGpuSource(selected, cropPlan)
: null;
if (!selected || !visible) {
generation += 1;
clearTimeout(transitionTimer);
painted = false;
previousVisible = false;
previousFading = false;
if (ready && renderer) renderer.clear();
return;
}
if (!ready) return;
if (!renderer) {
reportFailure(rendererError ?? new Error("live GPU renderer unavailable"), selected);
return;
}
// Preserve carousel crossfades within one artist, but never hold the
// previous artist's finished frame while the next artist is processing.
if (renderedScope !== selectedScope) {
renderedScope = selectedScope;
generation += 1;
clearTimeout(transitionTimer);
painted = false;
previousVisible = false;
previousFading = false;
renderer.clear();
}
const request = ++generation;
let cancelled = false;
onloading(selected);
(async () => {
try {
const payload = await payloadReady;
if (!payload) throw new Error("live hero payload unavailable");
if (cancelled || request !== generation || source !== selected) return;
const result = renderer.render(payload);
if (cancelled || request !== generation || source !== selected) return;
canvas.dataset.heroGpu = "ready";
canvas.dataset.heroSource = selected;
canvas.dataset.heroRenderMs = result.renderMs.toFixed(2);
canvas.dataset.heroRenderer = result.renderer;
clearTimeout(transitionTimer);
if (result.hadPrevious) {
previousVisible = true;
previousFading = false;
requestAnimationFrame(() => {
if (request !== generation) return;
// Commit a style pass at opacity 1 before starting the
// fade: rAF callbacks run before style recalc, so
// without this forced reflow the visible+fading class
// changes collapse into one recalc (opacity 0 -> 0)
// and the 1.4s transition never fires.
void previousCanvas.offsetWidth;
previousFading = true;
});
transitionTimer = setTimeout(() => {
previousVisible = false;
previousFading = false;
}, 1_400);
}
painted = true;
onready(selected, result);
} catch (error) {
if (!cancelled && request === generation && source === selected) {
canvas.dataset.heroGpu = "failed";
reportFailure(error, selected);
}
}
})();
return () => {
cancelled = true;
};
});
onDestroy(() => {
generation += 1;
clearTimeout(transitionTimer);
resizeObserver?.disconnect();
renderer?.destroy();
});
</script>
<!-- Each `previous` canvas must come AFTER its `current` sibling: they share
a z-index, so DOM order decides stacking, and the outgoing frame has to
overlay the already-updated current canvas while it fades out. -->
<div class="gpu-hero" class:active={Boolean(source)} class:painted aria-hidden="true">
<canvas bind:this={bloomCanvas} class="bloom current"></canvas>
<canvas bind:this={previousBloomCanvas} class="bloom previous" class:fading={previousFading} class:visible={previousVisible}></canvas>
<canvas bind:this={canvas} class="banner current"></canvas>
<canvas bind:this={previousCanvas} class="banner previous" class:fading={previousFading} class:visible={previousVisible}></canvas>
</div>
<style>
.gpu-hero {
position: relative;
display: block;
z-index: 1;
/* The 70px header strip must be PADDING, not a margin on the canvas:
this box has no border, so a child margin-top would collapse through
it and drag the whole wrapper — including the top-0 bloom strip —
70px down. That left only the static seed gradient behind the
frosted results-head, and offset the previous canvas 70px below the
current one during crossfades. */
padding-top: 70px;
opacity: 0;
transition: opacity 180ms ease;
}
/* Keep the WebGL context/shaders warm while metadata is resolving without
contributing a second 16:9 box beside a provisional <img>. */
.gpu-hero:not(.active) {
position: absolute;
inset: 0;
opacity: 0;
}
.gpu-hero.painted {
opacity: 1;
}
.gpu-hero.painted:not(.active) {
opacity: 0;
}
.banner {
position: relative;
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
max-height: 78vh;
z-index: 1;
}
.banner.previous {
position: absolute;
/* Matches the wrapper's 70px padding-top, so the outgoing frame sits
exactly over the in-flow current canvas. */
top: 70px;
left: 0;
opacity: 0;
pointer-events: none;
}
.banner.previous.visible {
opacity: 1;
}
.banner.previous.fading {
opacity: 0;
transition: opacity 1.4s ease;
}
.bloom {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 106px;
filter: blur(34px) saturate(1.6);
transform: scale(1.15);
opacity: 0.95;
z-index: 0;
}
.bloom.previous {
opacity: 0;
}
.bloom.previous.visible {
opacity: 0.95;
}
.bloom.previous.fading {
opacity: 0;
transition: opacity 1.4s ease;
}
@media (prefers-reduced-motion: reduce) {
.gpu-hero,
.banner.previous.fading,
.bloom.previous.fading {
transition-duration: 1ms;
}
}
</style>