changed style and increased performance

This commit is contained in:
Simon
2026-06-23 19:38:59 +00:00
parent d6865d7c35
commit 55828c9726
3 changed files with 120 additions and 36 deletions

View File

@@ -71,10 +71,26 @@ App.videos = App.videos || {};
const overflow = titleText.scrollWidth - titleWrap.clientWidth;
if (overflow > 4) {
card.classList.add('has-marquee');
titleText.style.setProperty('--marquee-distance', `${overflow + 12}px`);
const distance = overflow + 12;
titleText.style.setProperty('--marquee-distance', `${distance}px`);
// Drive the duration off the distance so every title scrolls at the
// same gentle speed (px/sec) instead of a fixed duration that made
// longer titles whip past. A floor keeps short titles from snapping.
const MARQUEE_SPEED = 28; // px per second
const MARQUEE_MIN_DURATION = 6; // seconds
const duration = Math.max(MARQUEE_MIN_DURATION, distance / MARQUEE_SPEED);
titleText.style.setProperty('--marquee-duration', `${duration.toFixed(2)}s`);
// Only marquee cards need the scroll-position observer that picks the
// centered card to animate; observing every card made scrolling a
// large grid needlessly expensive.
if (titleObserver) titleObserver.observe(card);
} else {
card.classList.remove('has-marquee', 'is-title-active');
titleText.style.removeProperty('--marquee-distance');
if (titleObserver) {
titleObserver.unobserve(card);
titleVisibility.delete(card);
}
}
updateTitleActive(card);
};
@@ -87,6 +103,9 @@ App.videos = App.videos || {};
document.querySelectorAll('.video-card').forEach((card) => {
measureTitle(card);
});
// Column width may have changed, re-wrapping titles and changing
// card heights, so re-run the (batched) full masonry pass.
App.videos.applyMasonryLayout();
});
};
@@ -322,7 +341,9 @@ App.videos = App.videos || {};
const thumb = card.querySelector('img');
App.videos.attachNoReferrerRetry(thumb);
if (thumb) {
thumb.addEventListener('load', App.videos.scheduleMasonryLayout);
// Only this card's height can change when its own thumbnail
// loads, so reposition just this card instead of the whole grid.
thumb.addEventListener('load', () => App.videos.layoutCard(card));
}
const favoriteBtn = card.querySelector('.favorite-btn');
if (favoriteBtn && favoriteKey) {
@@ -337,6 +358,7 @@ App.videos = App.videos || {};
if (titleWrap && titleText) {
requestAnimationFrame(() => {
measureTitle(card);
App.videos.layoutCard(card);
});
card.addEventListener('focusin', () => {
card.dataset.titleFocused = '1';
@@ -355,9 +377,9 @@ App.videos = App.videos || {};
card.dataset.titleHovered = '0';
updateTitleActive(card);
});
} else if (titleObserver) {
titleObserver.observe(card);
}
// On touch devices the marquee observer is attached lazily by
// measureTitle, but only for cards whose title actually overflows.
}
const uploaderBtn = card.querySelector('.uploader-link');
if (uploaderBtn) {
@@ -415,7 +437,8 @@ App.videos = App.videos || {};
state.renderedVideoIds.add(v.id);
});
App.videos.scheduleMasonryLayout();
// Each new card lays itself out via its own rAF / image-load handler
// above, so there is no need to relayout the whole grid here.
if (App.feed && typeof App.feed.renderSlides === 'function') {
App.feed.renderSlides();
}
@@ -506,29 +529,64 @@ App.videos = App.videos || {};
}
};
let masonryRaf = null;
App.videos.scheduleMasonryLayout = function() {
if (masonryRaf) {
cancelAnimationFrame(masonryRaf);
}
masonryRaf = requestAnimationFrame(() => {
masonryRaf = null;
App.videos.applyMasonryLayout();
});
// Grid track geometry is identical for every card and only changes when the
// viewport crosses a breakpoint, so we read it from the DOM once and cache
// it. Reading getComputedStyle per card (per image load) was a needless
// layout read on the hot path.
let cachedGridMetrics = null;
const getGridMetrics = function() {
if (cachedGridMetrics) return cachedGridMetrics;
const grid = document.getElementById('video-grid');
if (!grid) return null;
const styles = window.getComputedStyle(grid);
if (styles.display !== 'grid') return null;
const rowHeight = parseInt(styles.getPropertyValue('grid-auto-rows'), 10);
const rowGap = parseInt(styles.getPropertyValue('row-gap') || styles.getPropertyValue('gap'), 10) || 0;
if (!rowHeight) return null;
cachedGridMetrics = { rowHeight, rowGap };
return cachedGridMetrics;
};
const spanFor = function(itemHeight, metrics) {
return Math.ceil((itemHeight + metrics.rowGap) / (metrics.rowHeight + metrics.rowGap));
};
// Masonry placement for a single card. Each card's row span is independent
// of its siblings, so a newly loaded thumbnail only needs to re-measure its
// own card -- not relayout the entire (potentially huge) grid. This is the
// O(1) replacement for the old whole-grid pass that ran on every image load.
App.videos.layoutCard = function(card) {
if (!card) return;
const metrics = getGridMetrics();
if (!metrics) return;
const itemHeight = card.getBoundingClientRect().height;
if (!itemHeight) return;
card.style.gridRowEnd = `span ${spanFor(itemHeight, metrics)}`;
};
// Full relayout, used only when the column width actually changes (resize /
// breakpoint), since that re-wraps titles and changes every card's height.
// Reads are batched ahead of writes so we don't thrash layout per card the
// way the old per-item read-then-write loop did.
let masonryRaf = null;
App.videos.applyMasonryLayout = function() {
const grid = document.getElementById('video-grid');
if (!grid) return;
const styles = window.getComputedStyle(grid);
if (styles.display !== 'grid') return;
const rowHeight = parseInt(styles.getPropertyValue('grid-auto-rows'), 10);
const rowGap = parseInt(styles.getPropertyValue('row-gap') || styles.getPropertyValue('gap'), 10) || 0;
if (!rowHeight) return;
Array.from(grid.children).forEach((item) => {
const itemHeight = item.getBoundingClientRect().height;
const span = Math.ceil((itemHeight + rowGap) / (rowHeight + rowGap));
item.style.gridRowEnd = `span ${span}`;
cachedGridMetrics = null;
const metrics = getGridMetrics();
if (!metrics) return;
const cards = Array.from(grid.children);
const heights = cards.map((item) => item.getBoundingClientRect().height);
cards.forEach((item, i) => {
if (heights[i]) item.style.gridRowEnd = `span ${spanFor(heights[i], metrics)}`;
});
};
App.videos.scheduleMasonryLayout = function() {
if (masonryRaf) cancelAnimationFrame(masonryRaf);
masonryRaf = requestAnimationFrame(() => {
masonryRaf = null;
App.videos.applyMasonryLayout();
});
};