aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-22 10:08:17 -0700
committerAdam Soltys <[email protected]>2026-10-22 10:08:17 -0700
commit7947be155d4cc4f43dd89d743c5e09bfac4b635b (patch)
tree50caf83b4940167f231c98eb018b0b11d6669b35
parente7979230cdddde45113a7bf282f00b4192d2ab0a (diff)
downloadcoinosv3-7947be155d4cc4f43dd89d743c5e09bfac4b635b.tar.gz
coinosv3-7947be155d4cc4f43dd89d743c5e09bfac4b635b.zip
Videos grid, justified rows: a tile whose clip's size isn't known yet takes its still's proportions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/features/messages.js18
1 files changed, 16 insertions, 2 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index b209e35..34bc815 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -13926,7 +13926,18 @@ export function messagesFeature(ctx) {
const clipShape = (url) => { const z = clipSize(url); return z ? (z.h > z.w ? 'tall' : z.h < z.w ? 'wide' : 'square') : null; };
// width over height, kept between a phone's upright 9:16 and a cinema 2:1
// (a stray 32:9 or 1:4 would make a tile a sliver); unknown: a square
- const clipRatio = (url) => { const z = clipSize(url); return z ? Math.min(2, Math.max(0.5, z.w / z.h)) : 1; };
+ // (until the clip's size is known, the still's own proportions: the
+ // server's and a frame grab keep the clip's shape)
+ const posterAr = new Map();
+ const clampAr = (r) => Math.min(2, Math.max(0.5, r));
+ const clipRatio = (url) => { const z = clipSize(url); return z ? clampAr(z.w / z.h) : posterAr.get(url) || 1; };
+ function arFromStill(img, url) {
+ if (clipSize(url) || !img.naturalWidth || !img.naturalHeight) return;
+ const r = clampAr(img.naturalWidth / img.naturalHeight);
+ posterAr.set(url, r);
+ const tile = img.closest('.media-grid.justified .media-tile');
+ if (tile) tile.style.setProperty('--ar', r.toFixed(3));
+ }
// true/false, or null while the clip's shape isn't known
const clipFits = (url) => { if (clipsAll()) return true; const sh = clipShape(url); return sh === null ? null : sh === 'square' || sh === (screenTall() ? 'tall' : 'wide'); };
const clipsLabel = () => t(clipsAll() ? 'clipsAll' : screenTall() ? 'clipsTall' : 'clipsWide');
@@ -14236,6 +14247,7 @@ export function messagesFeature(ctx) {
mediaMode === 'images' || poster
? h('img', { src: poster || tileThumbs.get(it.url) || mediaSrc(it.url), alt: '', draggable: 'false',
loading: poster || tileThumbs.has(it.url) ? 'eager' : 'lazy', decoding: 'async',
+ onLoad: mediaMode === 'videos' ? (e) => arFromStill(e.currentTarget, it.url) : undefined,
onError: (e) => {
if (fromServer) { serverThumbFailed.add(it.url); render(); return; } // the frame grab's turn
const b = e.target.closest('.media-tile'); if (b) b.classList.add('broken');
@@ -14271,7 +14283,9 @@ export function messagesFeature(ctx) {
if (await grabFrame(url)) {
if (!v.isConnected) return;
const img = document.createElement('img');
- img.alt = ''; img.draggable = false; img.src = videoThumbs.get(url);
+ img.alt = ''; img.draggable = false;
+ img.addEventListener('load', () => arFromStill(img, url), { once: true });
+ img.src = videoThumbs.get(url);
v.replaceWith(img);
if (heavy && d.size) img.after(h('span', { class: 'media-tile-badge' }, heavyLabel(url)));
return;