/** * ViewportTracker — dual IntersectionObserver for page visibility detection. * * - visibleSet: pages currently on screen (rootMargin "0px") * - bufferSet: pages within ~2 viewport heights above/below (rootMargin "200% 0px") * * Calls onVisibilityChange(bufferSet, visibleSet) whenever either set changes. */ export class ViewportTracker { /** * @param {HTMLElement} root - scroll container (#canvas-container) * @param {HTMLElement[]} pageWrappers - array of .page-wrapper elements * @param {Function} onVisibilityChange - (bufferSet: Set, visibleSet: Set) => void */ constructor(root, pageWrappers, onVisibilityChange) { this._onVisibilityChange = onVisibilityChange; this._visibleSet = new Set(); this._bufferSet = new Set(); this._visibleObserver = null; this._bufferObserver = null; this._observe(root, pageWrappers); } _observe(root, pageWrappers) { const notify = () => { this._onVisibilityChange(new Set(this._bufferSet), new Set(this._visibleSet)); }; this._visibleObserver = new IntersectionObserver( entries => { for (const e of entries) { const page = parseInt(e.target.dataset.page, 10); if (e.isIntersecting) this._visibleSet.add(page); else this._visibleSet.delete(page); } notify(); }, { root, rootMargin: "0px", threshold: 0 } ); this._bufferObserver = new IntersectionObserver( entries => { for (const e of entries) { const page = parseInt(e.target.dataset.page, 10); if (e.isIntersecting) this._bufferSet.add(page); else this._bufferSet.delete(page); } notify(); }, { root, rootMargin: "200% 0px", threshold: 0 } ); for (const wrap of pageWrappers) { this._visibleObserver.observe(wrap); this._bufferObserver.observe(wrap); } } /** Observe a newly-added page wrapper. */ observe(wrap) { this._visibleObserver?.observe(wrap); this._bufferObserver?.observe(wrap); } disconnect() { this._visibleObserver?.disconnect(); this._bufferObserver?.disconnect(); this._visibleSet.clear(); this._bufferSet.clear(); } }