52 lines
2.5 KiB
JavaScript
52 lines
2.5 KiB
JavaScript
window.App = window.App || {};
|
|
App.marquee = App.marquee || {};
|
|
|
|
// A title is always one line. When it doesn't fit its box it scrolls sideways
|
|
// instead of wrapping or being silently cut off.
|
|
//
|
|
// Four surfaces show a title that way -- the grid card, the favorites bar card,
|
|
// the fullscreen player, the reels slide -- and they must scroll at the same
|
|
// speed to look like one app, so the measurement lives here rather than being
|
|
// written out again next to each of them. Whether a given title is *currently*
|
|
// scrolling is the caller's business (see the `is-title-active` handling in
|
|
// videos.js and favorites.js): a whole grid of marching text is unreadable, so
|
|
// most callers animate only the title the reader is actually looking at.
|
|
(function() {
|
|
// Drive the duration off the distance so every title scrolls at the same
|
|
// gentle rate rather than a fixed duration, which made longer titles whip
|
|
// past. The floor keeps short ones from snapping.
|
|
const SPEED_PX_PER_SEC = 28;
|
|
const MIN_DURATION_S = 6;
|
|
// Sub-pixel rounding isn't overflow worth animating.
|
|
const OVERFLOW_SLACK_PX = 4;
|
|
// Trailing space so the last word clears the edge before it wraps around.
|
|
const TAIL_GAP_PX = 12;
|
|
|
|
// Measures `text` inside `wrap` and prepares the animation: sets
|
|
// --marquee-distance / --marquee-duration on `text`, and marks `wrap` with
|
|
// `has-marquee` so CSS can decide what to do about it. Returns whether the
|
|
// title overflows -- callers use that to skip the bookkeeping (scroll
|
|
// observers, hover handlers) that only scrolling titles need.
|
|
//
|
|
// Reads layout, so call it when the element is in the document and visible;
|
|
// a hidden element measures as zero-width and reports no overflow.
|
|
App.marquee.measure = function(wrap, text) {
|
|
if (!wrap || !text) return false;
|
|
|
|
const overflow = text.scrollWidth - wrap.clientWidth;
|
|
if (overflow <= OVERFLOW_SLACK_PX) {
|
|
wrap.classList.remove('has-marquee');
|
|
text.style.removeProperty('--marquee-distance');
|
|
text.style.removeProperty('--marquee-duration');
|
|
return false;
|
|
}
|
|
|
|
const distance = overflow + TAIL_GAP_PX;
|
|
const duration = Math.max(MIN_DURATION_S, distance / SPEED_PX_PER_SEC);
|
|
text.style.setProperty('--marquee-distance', `${distance}px`);
|
|
text.style.setProperty('--marquee-duration', `${duration.toFixed(2)}s`);
|
|
wrap.classList.add('has-marquee');
|
|
return true;
|
|
};
|
|
})();
|