236 lines
7.1 KiB
JavaScript
236 lines
7.1 KiB
JavaScript
/**
|
|
* visibility-manager.js — Advanced visibility management with scroll prediction
|
|
*
|
|
* Uses IntersectionObserver for accurate page visibility detection
|
|
* Implements scroll prediction for smooth pre-loading
|
|
* Manages buffer zones intelligently
|
|
*/
|
|
|
|
class VisibilityManager {
|
|
constructor(container, pageElements, totalPages) {
|
|
this.container = container;
|
|
this.pageElements = pageElements;
|
|
this.totalPages = totalPages;
|
|
|
|
// Visibility state
|
|
this.visiblePages = new Set();
|
|
this.bufferPages = new Set();
|
|
this.observer = null;
|
|
this.bufferSize = 2; // Minimum pages to buffer
|
|
this.scrollHistory = [];
|
|
this.maxScrollHistory = 10;
|
|
|
|
// Scroll prediction
|
|
this.scrollVelocity = 0;
|
|
this.scrollDirection = 0; // 1 for down, -1 for up, 0 for stationary
|
|
this.lastScrollTime = 0;
|
|
this.lastScrollPosition = 0;
|
|
|
|
// Performance tracking
|
|
this.visibilityChanges = 0;
|
|
this.lastVisibilityTime = 0;
|
|
|
|
// Setup observers
|
|
this.setupIntersectionObserver();
|
|
this.setupScrollMonitoring();
|
|
}
|
|
|
|
setupIntersectionObserver() {
|
|
this.observer = new IntersectionObserver(
|
|
(entries) => this.handleIntersectionEntries(entries),
|
|
{
|
|
root: this.container,
|
|
rootMargin: '100% 0px 100% 0px', // Large margin for extensive buffering
|
|
threshold: [0, 0.25, 0.5, 0.75, 1.0] // Multiple thresholds for smooth transitions
|
|
}
|
|
);
|
|
|
|
// Observe all page elements
|
|
this.pageElements.forEach(element => {
|
|
this.observer.observe(element);
|
|
});
|
|
}
|
|
|
|
setupScrollMonitoring() {
|
|
// Track scroll events for velocity calculation
|
|
this.container.addEventListener('scroll', (event) => {
|
|
this.trackScrollEvent(event);
|
|
}, { passive: true });
|
|
|
|
// Periodic scroll prediction
|
|
setInterval(() => {
|
|
this.updateScrollPrediction();
|
|
}, 100);
|
|
}
|
|
|
|
trackScrollEvent(event) {
|
|
const currentTime = performance.now();
|
|
const currentPosition = this.container.scrollTop;
|
|
|
|
// Calculate velocity if we have previous data
|
|
if (this.lastScrollPosition !== 0 && this.lastScrollTime !== 0) {
|
|
const timeDelta = currentTime - this.lastScrollTime;
|
|
if (timeDelta > 0) {
|
|
this.scrollVelocity = (currentPosition - this.lastScrollPosition) / timeDelta;
|
|
this.scrollDirection = this.scrollVelocity > 0 ? 1 : (this.scrollVelocity < 0 ? -1 : 0);
|
|
}
|
|
}
|
|
|
|
// Store scroll history
|
|
this.scrollHistory.push({
|
|
position: currentPosition,
|
|
time: currentTime,
|
|
velocity: this.scrollVelocity
|
|
});
|
|
|
|
if (this.scrollHistory.length > this.maxScrollHistory) {
|
|
this.scrollHistory.shift();
|
|
}
|
|
|
|
this.lastScrollPosition = currentPosition;
|
|
this.lastScrollTime = currentTime;
|
|
}
|
|
|
|
updateScrollPrediction() {
|
|
if (this.scrollHistory.length < 3) return;
|
|
|
|
// Calculate average velocity from recent history
|
|
const recentHistory = this.scrollHistory.slice(-5); // Last 5 entries
|
|
if (recentHistory.length < 2) return;
|
|
|
|
const totalDelta = recentHistory[recentHistory.length - 1].position -
|
|
recentHistory[0].position;
|
|
const totalTime = recentHistory[recentHistory.length - 1].time -
|
|
recentHistory[0].time;
|
|
|
|
if (totalTime > 0) {
|
|
this.scrollVelocity = totalDelta / totalTime;
|
|
this.scrollDirection = this.scrollVelocity > 5 ? 1 :
|
|
(this.scrollVelocity < -5 ? -1 : 0);
|
|
}
|
|
}
|
|
|
|
handleIntersectionEntries(entries) {
|
|
const newVisiblePages = new Set();
|
|
const newBufferPages = new Set();
|
|
|
|
// Process intersection entries
|
|
entries.forEach(entry => {
|
|
const pageNum = parseInt(entry.target.dataset.page);
|
|
if (isNaN(pageNum)) return;
|
|
|
|
if (entry.isIntersecting) {
|
|
// Page is visible or in buffer zone
|
|
if (entry.intersectionRatio >= 0.5) {
|
|
newVisiblePages.add(pageNum);
|
|
} else {
|
|
newBufferPages.add(pageNum);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add intelligent buffering based on scroll prediction
|
|
this.addPredictiveBuffering(newVisiblePages, newBufferPages);
|
|
|
|
// Update state if changed
|
|
if (!this.setsEqual(this.visiblePages, newVisiblePages) ||
|
|
!this.setsEqual(this.bufferPages, newBufferPages)) {
|
|
|
|
this.visiblePages = newVisiblePages;
|
|
this.bufferPages = newBufferPages;
|
|
this.visibilityChanges++;
|
|
|
|
// Notify about visibility changes
|
|
this.notifyVisibilityChange();
|
|
}
|
|
}
|
|
|
|
addPredictiveBuffering(visiblePages, bufferPages) {
|
|
if (visiblePages.size === 0) return;
|
|
|
|
const visibleArray = Array.from(visiblePages).sort((a, b) => a - b);
|
|
const firstVisible = visibleArray[0];
|
|
const lastVisible = visibleArray[visibleArray.length - 1];
|
|
|
|
// Add adjacent pages (minimum buffer)
|
|
for (let i = 1; i <= this.bufferSize; i++) {
|
|
if (firstVisible - i >= 1) bufferPages.add(firstVisible - i);
|
|
if (lastVisible + i <= this.totalPages) bufferPages.add(lastVisible + i);
|
|
}
|
|
|
|
// Add scroll prediction-based buffering
|
|
if (Math.abs(this.scrollVelocity) > 10) { // Significant scrolling
|
|
const predictionDistance = Math.min(5, Math.floor(Math.abs(this.scrollVelocity) / 2));
|
|
|
|
if (this.scrollDirection === 1) { // Scrolling down
|
|
// Add more pages below
|
|
for (let i = 1; i <= predictionDistance; i++) {
|
|
const nextPage = lastVisible + this.bufferSize + i;
|
|
if (nextPage <= this.totalPages) {
|
|
bufferPages.add(nextPage);
|
|
}
|
|
}
|
|
} else if (this.scrollDirection === -1) { // Scrolling up
|
|
// Add more pages above
|
|
for (let i = 1; i <= predictionDistance; i++) {
|
|
const prevPage = firstVisible - this.bufferSize - i;
|
|
if (prevPage >= 1) {
|
|
bufferPages.add(prevPage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setsEqual(a, b) {
|
|
if (a.size !== b.size) return false;
|
|
return Array.from(a).every(item => b.has(item));
|
|
}
|
|
|
|
notifyVisibilityChange() {
|
|
// Throttle visibility notifications to prevent excessive rendering
|
|
const now = performance.now();
|
|
if (now - this.lastVisibilityTime < 50) return; // Max 20 updates per second
|
|
|
|
this.lastVisibilityTime = now;
|
|
|
|
// Notify the render system
|
|
if (window.renderSystem) {
|
|
window.renderSystem.setVisibility(
|
|
Array.from(this.visiblePages),
|
|
Array.from(this.bufferPages)
|
|
);
|
|
}
|
|
}
|
|
|
|
getScrollPrediction() {
|
|
return {
|
|
velocity: this.scrollVelocity,
|
|
direction: this.scrollDirection,
|
|
confidence: Math.min(1.0, Math.abs(this.scrollVelocity) / 20)
|
|
};
|
|
}
|
|
|
|
cleanup() {
|
|
if (this.observer) {
|
|
this.observer.disconnect();
|
|
}
|
|
|
|
this.visiblePages.clear();
|
|
this.bufferPages.clear();
|
|
this.scrollHistory = [];
|
|
}
|
|
|
|
getVisibilityStats() {
|
|
return {
|
|
visiblePages: Array.from(this.visiblePages).sort((a, b) => a - b),
|
|
bufferPages: Array.from(this.bufferPages).sort((a, b) => a - b),
|
|
visibilityChanges: this.visibilityChanges,
|
|
scrollVelocity: this.scrollVelocity,
|
|
scrollDirection: this.scrollDirection
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export for ES6 modules
|
|
export { VisibilityManager }; |