diff options
| author | Adam Soltys <[email protected]> | 2026-10-15 09:37:42 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-15 09:37:42 -0700 |
| commit | 393d4d77f03b015e2c15be19642cdd01364a3153 (patch) | |
| tree | a198f94426aaf28748a631f8b8ddf51e1acc73cc /src/features | |
| parent | 92c8b758c7b1876f59d79bef65ea38dffc41ca4b (diff) | |
| download | coinosv3-393d4d77f03b015e2c15be19642cdd01364a3153.tar.gz coinosv3-393d4d77f03b015e2c15be19642cdd01364a3153.zip | |
Media pager: a picture flies in as the feed's copy and is sharpened at rest; one zoom layer, not one per slide. A flight through the pager stuttered on the phone (Adam, 2026-10-16: "a bit of choppiness as they scroll"): every slide within a screen and a half loaded its picture's ORIGINAL as it passed — camera files of several megapixels, decoded on the main thread and uploaded as textures mid-flight — and every slide's <img> carried will-change: transform (for the pinch), a full-size layer each. Now a slide's <img> starts on the copy the feed painted (1100px wide, decoded already: mediaReady's thumb), or the server's copy (pic.webp s=col, cached at the CDN), or failing both the original (onError falls back to it too); decoding async. The slide at rest is sharpened a moment after landing (pagerSharpen: the original fetched and decoded aside, then swapped in — same box, same aspect), so a pinch has the detail, and a slide paged past costs nothing. Only the current slide (mp-cur, set in pager.arrive) has the layer. tools/pager-sharpen-test.js (three 4-megapixel pictures: every slide opens on a blob copy, the one on screen alone has will-change, it is sharpened to the original within a moment while the others keep their copies, ArrowDown moves the layer and sharpens the next); feed-picture-pager, pager-resize, pager-wheel, thread-pager and pager-swipe-fly tests pass.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 30 |
1 files changed, 29 insertions, 1 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index a6f49bb..d5da326 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12267,10 +12267,19 @@ export function messagesFeature(ctx) { function pagerSlide(it) { const { ev, url } = it; const video = pager.kind === 'videos'; + // A picture flies in as the feed's copy of it (1100px wide, decoded + // already if the feed painted it; else the server's, cached at the CDN) + // and is sharpened to the original once it comes to rest (pagerSharpen). + // Originals as they passed — camera files, several megapixels each — + // decoded on the main thread and were uploaded as textures mid-flight, + // and the flight stuttered (Adam, 2026-10-16: "a bit of choppiness as + // they scroll"). A copy that fails falls back to the original. + const quick = !video && !/\.gif(\?|$)/i.test(url) ? (mediaReady.get(url) || {}).thumb || serverPic(url, 'col') || url : url; const media = video ? h('video', { class: 'mp-media', 'data-src': url, loop: true, playsinline: true, preload: 'none', poster: videoThumbs.get(url) || posterOf(ev, url) || (videoInfo.get(url) || {}).image || serverThumb(url) }) - : h('img', { class: 'mp-media', 'data-src': url, alt: '', draggable: 'false' }); + : h('img', { class: 'mp-media', 'data-src': quick, 'data-full': url, alt: '', draggable: 'false', decoding: 'async', + onError: (e) => { const m = e.currentTarget; if ((m.getAttribute('src') || '') !== url) { m.setAttribute('data-src', url); m.setAttribute('src', url); } } }); const caption = mediaCaption(ev); const stop = (fn) => (e) => { e.stopPropagation(); fn(e); }; const slide = h('div', { class: 'mp-slide', 'data-k': mediaKey(it) }, @@ -12393,6 +12402,22 @@ export function messagesFeature(ctx) { const m = slide.querySelector('.mp-media'); if (m && !m.getAttribute('src')) m.setAttribute('src', m.tagName === 'VIDEO' ? playSrc(m.getAttribute('data-src')) : m.getAttribute('data-src')); } + // The slide at rest gets its picture's original — fetched and decoded + // aside first, then swapped in (same box, same aspect: nothing moves) — + // a moment after landing, so a quick page past it costs nothing. + function pagerSharpen(slide) { + const m = slide && slide.querySelector('img.mp-media'); + const full = m && m.getAttribute('data-full'); + if (!full || m.getAttribute('src') === full || m._sharp) return; + m._sharp = setTimeout(() => { + m._sharp = 0; + if (!pager || pager.cur !== slide) return; + const im = new Image(); + im.decoding = 'async'; + im.onload = () => { const go = () => { if (pager && m.isConnected && m.getAttribute('src') !== full) { m.setAttribute('data-src', full); m.setAttribute('src', full); } }; (im.decode ? im.decode().then(go, go) : go()); }; + im.src = full; + }, 300); + } function pagerRelease(slide) { const m = slide.querySelector('video.mp-media'); if (m && m.getAttribute('src')) { try { m.pause(); m.removeAttribute('src'); m.load(); } catch {} } @@ -12908,9 +12933,12 @@ export function messagesFeature(ctx) { if (v && pager.curAt) { const secs = (Date.now() - pager.curAt) / 1000; dwell = dwell ? dwell * 0.7 + secs * 0.3 : secs; } if (pagerSource() && video) el.classList.remove('show'); } + if (pager.cur) pager.cur.classList.remove('mp-cur'); + slide.classList.add('mp-cur'); // (the one with the layer for a pinch: style.css) pager.cur = slide; pager.anchor = slide; pager.curAt = Date.now(); + pagerSharpen(slide); pagerArrows(slide); pagerPosition(); if (pager.fromFeed) { const row = feedNow().notes.find((x) => x.id === slide._rowId); markSeen(row || slide._ev); } |
