3a4270e6be
The buffered cascade revealed tiles on an 80ms timer regardless of image load, so the flip-in animation played on a gray placeholder and the thumbnail popped in afterward. Worse, MasonryGrid ALSO applied a per-index animation-delay (index×70ms) that compounded on top of the insert cadence, so the cascade visibly dragged and desynced as it grew. Now the producer preloads each queued thumbnail (decode pipelined ahead) and the consumer awaits that decode before pushing the item — every tile animates in fully loaded, strictly one at a time. Drop the compounding CSS stagger; the store's one-item-at-a-time push is the sole pacer, so each tile animates the instant it mounts. New utils/preloadImage.js (load+decode+timeout gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
22 lines
1004 B
JavaScript
22 lines
1004 B
JavaScript
// Resolve once the image at `url` is fully loaded AND decoded (paint-ready),
|
|
// or after `timeoutMs` as a safety net so a broken/slow image never stalls a
|
|
// cascade. Never rejects — the caller only cares that it's safe to reveal.
|
|
//
|
|
// Used by the showcase cascade to gate each tile's entry animation on the
|
|
// thumbnail actually being ready, so the flip-in plays on a real image rather
|
|
// than on a gray placeholder (operator-flagged 2026-06-04).
|
|
export function preloadImage (url, timeoutMs = 4000) {
|
|
return new Promise((resolve) => {
|
|
let done = false
|
|
const finish = () => { if (!done) { done = true; resolve() } }
|
|
const img = new Image()
|
|
img.onload = finish
|
|
img.onerror = finish
|
|
img.src = url
|
|
// decode() resolves at paint-ready (a beat after onload); prefer it when
|
|
// available, but onload/onerror/timeout all still settle the promise.
|
|
if (typeof img.decode === 'function') img.decode().then(finish).catch(() => {})
|
|
setTimeout(finish, timeoutMs)
|
|
})
|
|
}
|