improve tiktok mode
This commit is contained in:
@@ -539,14 +539,18 @@ App.videos = App.videos || {};
|
||||
// * mounting/unmounting a card never moves any other card -- positions are
|
||||
// assigned once and never change -- so there is no scroll jank.
|
||||
//
|
||||
// Card heights are deterministic (single-line title, fixed 16:9 thumbnail,
|
||||
// optional tag/uploader/duration rows), so we measure one card per distinct
|
||||
// "shape" and reuse that height. Packing is shortest-column-first.
|
||||
// Thumbnails keep their natural aspect ratio (not cropped), so a card's real
|
||||
// height isn't known until its image loads. We place each card with a 16:9
|
||||
// estimate first, then once the image loads we correct that card's height
|
||||
// and shift only the cards below it *in the same column* (each column is an
|
||||
// independent vertical stack), so a correction never disturbs other columns
|
||||
// or anything above it. Columns are assigned once and never change.
|
||||
// ---------------------------------------------------------------------
|
||||
App.virtualGrid = (function() {
|
||||
const mounted = new Map(); // loadedVideos index -> card element
|
||||
const layout = []; // index -> { top, left, width, height }
|
||||
const heightCache = new Map(); // shape signature -> measured px height
|
||||
const layout = []; // index -> { top, left, width, height, col, posInCol }
|
||||
const heightCache = new Map(); // shape signature -> estimated px height
|
||||
let colItems = []; // col -> ordered list of item indices in that column
|
||||
let colBottoms = []; // running bottom y of each column
|
||||
let cols = 0, colWidth = 0, gap = 16, padX = 24, padY = 24;
|
||||
let initialized = false;
|
||||
@@ -582,8 +586,10 @@ App.videos = App.videos || {};
|
||||
v.uploader ? 1 : 0, (v.duration > 0) ? 1 : 0].join('|');
|
||||
};
|
||||
|
||||
// Height of a card of `v`'s shape at the current column width. Measured
|
||||
// once per shape via a hidden probe, then cached.
|
||||
// Estimated height of a card of `v`'s shape at the current column width,
|
||||
// using the CSS 16:9 thumbnail placeholder (the image isn't loaded in the
|
||||
// probe). Measured once per shape, then cached. The real height replaces
|
||||
// this per-card once the thumbnail loads (see correct()).
|
||||
const heightOf = function(v) {
|
||||
const sig = signatureOf(v);
|
||||
const cached = heightCache.get(sig);
|
||||
@@ -611,10 +617,12 @@ App.videos = App.videos || {};
|
||||
};
|
||||
|
||||
// Assigns positions to items [start, end). Earlier items keep their
|
||||
// positions because colBottoms carries forward unchanged.
|
||||
// positions because colBottoms carries forward unchanged. Each item is
|
||||
// assigned to the currently-shortest column and stays there for good.
|
||||
const packFrom = function(start) {
|
||||
if (!cols) { if (!measureMetrics()) return; }
|
||||
if (!colBottoms.length) colBottoms = new Array(cols).fill(padY);
|
||||
if (!colItems.length) colItems = Array.from({ length: cols }, () => []);
|
||||
for (let i = start; i < state.loadedVideos.length; i++) {
|
||||
const v = state.loadedVideos[i];
|
||||
const h = heightOf(v);
|
||||
@@ -627,13 +635,39 @@ App.videos = App.videos || {};
|
||||
top,
|
||||
left: padX + col * (colWidth + gap),
|
||||
width: colWidth,
|
||||
height: h
|
||||
height: h,
|
||||
col,
|
||||
posInCol: colItems[col].length
|
||||
};
|
||||
colItems[col].push(i);
|
||||
colBottoms[col] = top + h + gap;
|
||||
}
|
||||
setContainerHeight();
|
||||
};
|
||||
|
||||
// Replaces item i's estimated height with its real (post-image-load)
|
||||
// height and slides every card below it in the same column by the delta.
|
||||
// Other columns and everything above are untouched -> no global reflow.
|
||||
const correct = function(i) {
|
||||
const card = mounted.get(i);
|
||||
const l = layout[i];
|
||||
if (!card || !l) return;
|
||||
const real = card.getBoundingClientRect().height;
|
||||
if (!real || Math.abs(real - l.height) < 1) return;
|
||||
const delta = real - l.height;
|
||||
l.height = real;
|
||||
const list = colItems[l.col];
|
||||
for (let k = l.posInCol + 1; k < list.length; k++) {
|
||||
const j = list[k];
|
||||
layout[j].top += delta;
|
||||
const mc = mounted.get(j);
|
||||
if (mc) mc.style.top = layout[j].top + 'px';
|
||||
}
|
||||
colBottoms[l.col] += delta;
|
||||
setContainerHeight();
|
||||
scheduleUpdate();
|
||||
};
|
||||
|
||||
const place = function(card, l) {
|
||||
card.style.position = 'absolute';
|
||||
card.style.top = l.top + 'px';
|
||||
@@ -650,6 +684,17 @@ App.videos = App.videos || {};
|
||||
place(card, l);
|
||||
grid().appendChild(card);
|
||||
mounted.set(i, card);
|
||||
// Once the thumbnail loads, drop the 16:9 placeholder so it shows at
|
||||
// its true aspect ratio, then correct this card's height.
|
||||
const img = card.querySelector('img');
|
||||
if (img) {
|
||||
const reveal = () => {
|
||||
img.style.aspectRatio = 'auto';
|
||||
if (mounted.get(i) === card) correct(i);
|
||||
};
|
||||
if (img.complete && img.naturalHeight > 0) requestAnimationFrame(reveal);
|
||||
else img.addEventListener('load', reveal);
|
||||
}
|
||||
// Marquee + direct-playability probe only matter for on-screen cards.
|
||||
requestAnimationFrame(() => { if (mounted.get(i) === card) measureTitle(card); });
|
||||
probeObserver.observe(card);
|
||||
@@ -699,6 +744,7 @@ App.videos = App.videos || {};
|
||||
mounted.forEach((card, i) => unmount(i));
|
||||
layout.length = 0;
|
||||
colBottoms = new Array(cols).fill(padY);
|
||||
colItems = Array.from({ length: cols }, () => []);
|
||||
packFrom(0);
|
||||
update();
|
||||
};
|
||||
@@ -720,6 +766,7 @@ App.videos = App.videos || {};
|
||||
mounted.clear();
|
||||
layout.length = 0;
|
||||
colBottoms = [];
|
||||
colItems = [];
|
||||
const el = grid();
|
||||
if (el) { el.innerHTML = ''; el.style.height = '0px'; }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user