web: keep the film strip seamless on a wide window
The marquee loops by translating the track by half its width, so the first half has to be at least as wide as the window. A short reel — seven stills, about 1780px — covered a 1440px window but not a 1920px or 2560px one: the strip ran out of frames before the loop restarted, and the band on the right stayed blank until the next pass drifted in. The track now measures one frame's pitch and repeats the reel until a half covers the window, re-measuring on resize. One still in the strip is repeated enough times to loop cleanly on its own; a reel already wide enough is left at a single copy, so nothing is duplicated without cause.
This commit is contained in:
@@ -317,6 +317,27 @@ export function Landing() {
|
||||
[photos],
|
||||
);
|
||||
|
||||
// The marquee loops by translating half the track, so one half has to be at
|
||||
// least as wide as the window: a reel too short for the screen runs out of
|
||||
// frames before the loop restarts, and the band on the right goes blank until
|
||||
// the next pass arrives. Lay out one pass, then repeat the reel until a half
|
||||
// covers the window — and again on resize, which changes what "covers" means.
|
||||
const track = useRef<HTMLDivElement>(null);
|
||||
const [reelCopies, setReelCopies] = useState(1);
|
||||
useEffect(() => {
|
||||
const el = track.current;
|
||||
const fan = el?.querySelector<HTMLElement>('.lp-frame');
|
||||
if (!el || !fan || !reel.length) return;
|
||||
const pitch = fan.getBoundingClientRect().width + (parseFloat(getComputedStyle(fan).marginRight) || 0);
|
||||
if (pitch <= 0) return;
|
||||
const view = () => el.parentElement?.clientWidth || window.innerWidth;
|
||||
const fit = () => setReelCopies(Math.max(1, Math.ceil(view() / (reel.length * pitch))));
|
||||
fit();
|
||||
window.addEventListener('resize', fit);
|
||||
return () => window.removeEventListener('resize', fit);
|
||||
}, [reel]);
|
||||
const loop = useMemo(() => Array.from({ length: reelCopies }, () => reel).flat(), [reel, reelCopies]);
|
||||
|
||||
// The three live sections, each cycling the stills the curator tagged for its
|
||||
// slot. Every set is drawn from a random start once per page load, so the
|
||||
// preview does not change under the visitor's eyes on every render; a slot
|
||||
@@ -643,8 +664,8 @@ export function Landing() {
|
||||
<p>{c({ en: 'Every still here is picked and placed by the studio — this reel is curated, not crowd-sourced.', vi: 'Mọi khung ảnh ở đây đều do studio chọn và đặt — dải phim này được tuyển chọn, không phải ai cũng tự thêm được.' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lp-track">
|
||||
{[...reel, ...reel].map((look, i) => {
|
||||
<div className="lp-track" ref={track}>
|
||||
{[...loop, ...loop].map((look, i) => {
|
||||
const score = ratings[look.rateKey] ?? { avg: 0, n: 0, mine: 0 };
|
||||
return (
|
||||
<article className="lp-frame" key={`${look.tag}-${i}`}>
|
||||
|
||||
Reference in New Issue
Block a user