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