velocity based queue
This commit is contained in:
@@ -46,6 +46,10 @@ interface RenderQueueRequest {
|
||||
}
|
||||
|
||||
const RERENDER_DELTA = 48;
|
||||
const MAX_PIXEL_RATIO = 2;
|
||||
const FAST_SCROLL_VELOCITY_THRESHOLD = 0.8; // px/ms ~ 800px/s
|
||||
const FAST_SCROLL_DWELL_THRESHOLD_MS = 120;
|
||||
const SCROLL_VELOCITY_MIN_DELTA = 0.05;
|
||||
|
||||
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
|
||||
const viewportStyle = window.getComputedStyle(viewportNode);
|
||||
@@ -87,6 +91,7 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
const [pixelRatio, setPixelRatio] = useState(1);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('contain');
|
||||
const [visiblePages, setVisiblePages] = useState<Set<number>>(new Set());
|
||||
const [scrollVelocity, setScrollVelocity] = useState(0);
|
||||
const pdfRef = useRef<PDFDocumentProxy | null>(null);
|
||||
const wasmUrlRef = useRef<string | null>(null);
|
||||
const pageNodeMapRef = useRef<Map<number, HTMLElement>>(new Map());
|
||||
@@ -94,6 +99,12 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
const renderQueueRef = useRef<RenderQueueRequest[]>([]);
|
||||
const activeRendersRef = useRef(new Set<number>());
|
||||
const maxConcurrentRendersRef = useRef(2);
|
||||
const scrollVelocityRef = useRef({
|
||||
lastPosition: 0,
|
||||
lastTime: 0,
|
||||
velocity: 0,
|
||||
});
|
||||
const scrollElementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const requestQueueFlush = useCallback(() => {
|
||||
const queue = renderQueueRef.current;
|
||||
@@ -277,7 +288,7 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
return;
|
||||
}
|
||||
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const ratio = Math.min(window.devicePixelRatio || 1, MAX_PIXEL_RATIO);
|
||||
const descriptors: PageDescriptor[] = [];
|
||||
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
|
||||
if (cancelled) {
|
||||
@@ -401,6 +412,20 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
|
||||
const intersectionRoot = viewportRef?.current || null;
|
||||
|
||||
const updateScrollVelocityState = useCallback((nextVelocity: number) => {
|
||||
setScrollVelocity((previous) => {
|
||||
const wasFast = Math.abs(previous) > FAST_SCROLL_VELOCITY_THRESHOLD;
|
||||
const isFast = Math.abs(nextVelocity) > FAST_SCROLL_VELOCITY_THRESHOLD;
|
||||
if (!isFast && !wasFast && Math.abs(previous - nextVelocity) < SCROLL_VELOCITY_MIN_DELTA) {
|
||||
return previous;
|
||||
}
|
||||
if (Math.abs(previous - nextVelocity) < SCROLL_VELOCITY_MIN_DELTA && wasFast === isFast) {
|
||||
return previous;
|
||||
}
|
||||
return nextVelocity;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const registerPageNode = useCallback((pageNumber: number, node: HTMLElement | null) => {
|
||||
const map = pageNodeMapRef.current;
|
||||
const existing = map.get(pageNumber);
|
||||
@@ -421,6 +446,8 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
useEffect(() => {
|
||||
let frame: number | null = null;
|
||||
let observer: IntersectionObserver | null = null;
|
||||
let scrollElement: HTMLElement | null = null;
|
||||
let scrollListener: (() => void) | null = null;
|
||||
|
||||
const attachObserver = () => {
|
||||
const stackElement = containerRef.current;
|
||||
@@ -431,6 +458,34 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
const root = intersectionRoot
|
||||
|| stackElement.closest('.document-viewer__viewport')
|
||||
|| undefined;
|
||||
scrollElement = (viewportRef?.current || stackElement.closest('.document-viewer__viewport')) ?? null;
|
||||
scrollElementRef.current = scrollElement;
|
||||
if (scrollElement && !scrollListener) {
|
||||
scrollVelocityRef.current = {
|
||||
lastPosition: scrollElement.scrollTop,
|
||||
lastTime: performance.now(),
|
||||
velocity: 0,
|
||||
};
|
||||
scrollListener = () => {
|
||||
if (!scrollElement) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const position = scrollElement.scrollTop;
|
||||
const elapsed = now - scrollVelocityRef.current.lastTime;
|
||||
if (elapsed <= 0) {
|
||||
return;
|
||||
}
|
||||
const velocity = (position - scrollVelocityRef.current.lastPosition) / elapsed;
|
||||
scrollVelocityRef.current = {
|
||||
lastPosition: position,
|
||||
lastTime: now,
|
||||
velocity,
|
||||
};
|
||||
updateScrollVelocityState(velocity);
|
||||
};
|
||||
scrollElement.addEventListener('scroll', scrollListener, { passive: true });
|
||||
}
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const attr = entry.target.getAttribute('data-page-number');
|
||||
@@ -462,8 +517,14 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
if (intersectionObserverRef.current === observer) {
|
||||
intersectionObserverRef.current = null;
|
||||
}
|
||||
if (scrollElement && scrollListener) {
|
||||
scrollElement.removeEventListener('scroll', scrollListener);
|
||||
}
|
||||
if (scrollElementRef.current === scrollElement) {
|
||||
scrollElementRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [handlePageVisibilityChange, intersectionRoot]);
|
||||
}, [handlePageVisibilityChange, intersectionRoot, updateScrollVelocityState, viewportRef]);
|
||||
|
||||
const pageElements = useMemo(() => (
|
||||
pages.map((page) => (
|
||||
@@ -481,11 +542,13 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
enqueueRender={enqueueRender}
|
||||
cancelRenderRequest={cancelRenderRequest}
|
||||
onRenderFinished={handleRenderFinished}
|
||||
scrollVelocity={scrollVelocity}
|
||||
/>
|
||||
</Fragment>
|
||||
))
|
||||
), [cancelRenderRequest, enqueueRender, handlePageClick, handleRenderFinished,
|
||||
pages, pixelRatio, registerPageNode, shouldRenderPage, viewportHeight, viewportWidth, viewMode]);
|
||||
pages, pixelRatio, registerPageNode, scrollVelocity, shouldRenderPage, viewportHeight, viewportWidth,
|
||||
viewMode]);
|
||||
|
||||
return (
|
||||
<div className={viewerClasses}>
|
||||
@@ -522,6 +585,7 @@ interface PdfPageCanvasProps {
|
||||
enqueueRender: (request: RenderQueueRequest) => void;
|
||||
cancelRenderRequest: (pageNumber: number) => void;
|
||||
onRenderFinished: (pageNumber: number) => void;
|
||||
scrollVelocity: number;
|
||||
}
|
||||
|
||||
function PdfPageCanvas({
|
||||
@@ -537,6 +601,7 @@ function PdfPageCanvas({
|
||||
enqueueRender,
|
||||
cancelRenderRequest,
|
||||
onRenderFinished,
|
||||
scrollVelocity,
|
||||
}: PdfPageCanvasProps): JSX.Element {
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
@@ -609,6 +674,19 @@ function PdfPageCanvas({
|
||||
}
|
||||
}, [descriptor.number, descriptor.scale, onRenderFinished, pdfRef, pixelRatio]);
|
||||
|
||||
const meetsVelocityRequirement = useMemo(() => {
|
||||
if (!shouldRender) {
|
||||
return true;
|
||||
}
|
||||
const absoluteVelocity = Math.abs(scrollVelocity);
|
||||
if (absoluteVelocity <= FAST_SCROLL_VELOCITY_THRESHOLD) {
|
||||
return true;
|
||||
}
|
||||
const estimatedMs = (descriptor.height + viewportHeight)
|
||||
/ Math.max(absoluteVelocity, 0.001);
|
||||
return estimatedMs >= FAST_SCROLL_DWELL_THRESHOLD_MS;
|
||||
}, [descriptor.height, scrollVelocity, shouldRender, viewportHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasMounted) {
|
||||
return undefined;
|
||||
@@ -635,7 +713,7 @@ function PdfPageCanvas({
|
||||
}, [descriptor.number, resetCanvas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRender) {
|
||||
if (!shouldRender || !meetsVelocityRequirement) {
|
||||
setAllowRender(false);
|
||||
cancelRenderRequest(descriptor.number);
|
||||
return;
|
||||
@@ -660,7 +738,7 @@ function PdfPageCanvas({
|
||||
setAllowRender(false);
|
||||
cancelRenderRequest(descriptor.number);
|
||||
};
|
||||
}, [cancelRenderRequest, descriptor.number, enqueueRender, shouldRender]);
|
||||
}, [cancelRenderRequest, descriptor.number, enqueueRender, meetsVelocityRequirement, shouldRender]);
|
||||
|
||||
useEffect(() => {
|
||||
if (allowRender) {
|
||||
|
||||
Reference in New Issue
Block a user