diff options
| author | Adam Soltys <[email protected]> | 2026-09-04 18:37:53 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-04 18:37:53 -0700 |
| commit | 4431198900b2d1a5cfb73bd02ddfd1e377d9acb9 (patch) | |
| tree | b285d431f7d17d6ceee8f630dad4b8a043c628f5 /src/features/hats.js | |
| parent | ff5c5d2ac5c017c7c134f3969416306cd8742d1c (diff) | |
| download | coinosv3-4431198900b2d1a5cfb73bd02ddfd1e377d9acb9.tar.gz coinosv3-4431198900b2d1a5cfb73bd02ddfd1e377d9acb9.zip | |
Hats no longer pop in a beat after page load. The hats feature is a deferred chunk, so every boot painted bare heads until it arrived — but the core bundle can't carry the hat art. Instead, wrapAvatar now leaves a STAMP per head in localStorage (art + percent geometry, size-independent, capped at 40, cleared when a head goes bare so removals can't ghost), and the core avatar factory replays stamps until hatsReady answers — from then on a null from wrapAvatar genuinely means bare. Verified headless: a stamped hat is on the header avatar 15ms after DOM-ready, and once the chunk lands with a bare-head answer the stamp is cleared with no double-wrap
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src/features/hats.js')
| -rw-r--r-- | src/features/hats.js | 38 |
1 files changed, 38 insertions, 0 deletions
diff --git a/src/features/hats.js b/src/features/hats.js index c8b27b6..22b573a 100644 --- a/src/features/hats.js +++ b/src/features/hats.js @@ -134,6 +134,40 @@ export function hatsFeature(ctx) { }, 800); }; + // ---- boot stamps --------------------------------------------------------- + // This feature is a DEFERRED chunk — for the beat between first paint and + // the chunk landing, avatars render bare and hats popped in late. The core + // bundle can't hold the hat art, so wrapAvatar leaves a STAMP per head + // (art + percent geometry, size-independent) in localStorage, and the + // avatar factory replays stamps until this chunk takes over. + let stamps = null, stampT = 0; + const stampStore = () => { + if (stamps === null) { try { stamps = JSON.parse(localStorage.getItem('hat-stamps') || '{}'); } catch { stamps = {}; } } + return stamps; + }; + const stamp = (pk, hat) => { + const s = stampStore(); + if (!hat) { + // a bare head clears its stamp, or a removed hat would ghost on boot + if (s[pk]) { delete s[pk]; saveStamps(); } + return; + } + const next = { s: posStyle(hat), a: hat.art, t: Date.now() }; + if (!s[pk] || s[pk].a !== next.a || s[pk].s !== next.s) { + s[pk] = next; + const keys = Object.keys(s); + if (keys.length > 40) { + keys.sort((a, b) => (s[a].t || 0) - (s[b].t || 0)); + for (const k of keys.slice(0, keys.length - 40)) delete s[k]; + } + saveStamps(); + } else s[pk].t = Date.now(); + }; + const saveStamps = () => { + clearTimeout(stampT); + stampT = setTimeout(() => { try { localStorage.setItem('hat-stamps', JSON.stringify(stamps)); } catch {} }, 500); + }; + const pending = new Set(); let fetchT = 0, failedAt = 0; const queueFetch = (pk) => { @@ -373,8 +407,12 @@ export function hatsFeature(ctx) { id: 'hats', // The avatar factory (and the search rows) offer every avatar up for // decoration; a bare head comes back null and is used as-is. + // Present the moment this chunk lands — the avatar factory stops + // replaying boot stamps once a real answer (hat or bare) is available. + hatsReady() { return true; }, wrapAvatar(pk, node) { const hat = hatById(hatFor(pk)); + stamp(pk, hat); // keep the boot stamp current (or clear it) if (!hat) return null; return h('span', { class: 'hat-wrap' }, node, |
