diff --git a/src/views/CatalogArtist.svelte b/src/views/CatalogArtist.svelte index a235b7e..f85c6f8 100644 --- a/src/views/CatalogArtist.svelte +++ b/src/views/CatalogArtist.svelte @@ -322,6 +322,40 @@ } } + /** Re-bake the twins when the hero's box has changed size under them. + * + * The twin is a BAKED copy of the hero canvas, so it keeps the dimensions + * it was baked at. snapshotGpuTwins only ever ran on a hero READY, so after + * a window or panel resize the live hero repainted at the new size while + * the twin went on stretching its old frame into the new box — the mono + * overlay sat visibly off the photograph, and stayed there until the next + * hero happened to re-bake it. (Kyle: "it happens on resize and it's + * permanent until the hero changes.") + * + * Dimension-guarded rather than fired on every resize tick, so it costs + * nothing when the box moves without the backing store following — which + * is most of a drag, since the renderer only re-sizes its store once the + * box settles. */ + function rebakeTwinsIfStale() { + const source = document.querySelector(".artist-page .gpu-hero canvas.banner.current"); + const baked = gpuTwinBox?.lastElementChild; + if (!source || !source.width || !baked) return; + if (baked.width === source.width && baked.height === source.height) return; + snapshotGpuTwins(); + } + + let twinRebakeTimer = 0; + $effect(() => { + // Re-runs whenever the hero's measured box changes. + const width = heroWidth; + if (!width) return; + clearTimeout(twinRebakeTimer); + // Past the renderer's own 140ms settle, so the canvas being copied is + // the one already repainted at the new size. + twinRebakeTimer = setTimeout(rebakeTwinsIfStale, 260); + return () => clearTimeout(twinRebakeTimer); + }); + const HERO_ROTATE_MS = 20_000; const HERO_FADE_MS = 1_400; // The preview→finished 4K swap gets its own, shorter dissolve: long enough @@ -5416,6 +5450,27 @@ animation-range: 0px var(--travel, 999999px); } + /* The blurred load seed and its mono twin ride with the photograph. + They were left out of the rule above because hero-squeeze-ride sets + `transform` outright and would drop the scale(1.06) they are built + on; hero-seed-ride is the same translate composed AHEAD of that + scale, so the offset stays in unscaled pixels and matches the + banner's exactly. + + Without it the seed held its rest position while the hero rode up — + measured 132px adrift at scrollTop 600 — so whenever the seed was on + screen at a non-zero scroll the blur sat off the photograph. It + appeared to "heal itself" only because a new hero hides the seed. */ + .artist-page[data-scroll="1"] .hero-seed, + .artist-page[data-scroll="1"] .seed-mono-twin { + animation-name: hero-seed-ride; + animation-duration: auto; + animation-timing-function: linear; + animation-fill-mode: both; + animation-timeline: --artist-pane; + animation-range: 0px var(--travel, 999999px); + } + /* The seam tail is glued to the banner's edge: SAME squeeze ride, SAME gate, so they attach on the same flip and move as one. (No mono ride — the tail is the artwork's own colour by design and @@ -5481,8 +5536,9 @@ animation-range: 0px var(--travel, 999999px); } - /* (.hero-seed rides nothing here: its mono look is the seed twin's - crossfade below, exactly like the banner's.) */ + /* (.hero-seed's MONO look rides nothing here: that is the seed twin's + crossfade below, exactly like the banner's. Its squeeze ride is the + rule above — see hero-seed-ride.) */ /* The sheet, ungated like today's calc — it must track mid-scroll even under main-thread stall. The static transform is the REST @@ -5846,6 +5902,18 @@ } } + /* hero-squeeze-ride for the layers that carry their own scale. Translate + FIRST, so it is applied in unscaled space and the seed moves the same + number of screen pixels as the banner rather than 1.06x that. */ + @keyframes -global-hero-seed-ride { + from { + transform: translateY(0) scale(1.06); + } + to { + transform: translateY(calc(-0.22 * var(--ride, 0px))) scale(1.06); + } + } + @keyframes ground-mono-ride { from { filter: grayscale(0) brightness(1);