Use animation frames for auto scrolling continuous pagers
This commit is contained in:
@@ -10,20 +10,57 @@ import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } f
|
|||||||
import { ScrollDirection } from '@/modules/core/Core.types.ts';
|
import { ScrollDirection } from '@/modules/core/Core.types.ts';
|
||||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||||
|
|
||||||
// in case the scroll amount is not enough "scrollBy" just does not do anything
|
|
||||||
const MIN_SCROLL_AMOUNT_PX = 1.5;
|
const MIN_SCROLL_AMOUNT_PX = 1.5;
|
||||||
const getScrollAmount = (amountPerMs: number, speedMs: number): { amountPx: number; speedMs: number } => {
|
const getScrollAmount = (
|
||||||
|
amountPerMs: number,
|
||||||
|
speedMs: number,
|
||||||
|
isRTL: boolean = false,
|
||||||
|
invert: boolean = false,
|
||||||
|
): { amountPx: number; speedMs: number } => {
|
||||||
const amountPx = amountPerMs * speedMs;
|
const amountPx = amountPerMs * speedMs;
|
||||||
|
|
||||||
if (amountPx >= MIN_SCROLL_AMOUNT_PX) {
|
if (amountPx >= MIN_SCROLL_AMOUNT_PX) {
|
||||||
return { amountPx, speedMs };
|
const pxPerMsReadingMode = isRTL ? -amountPx : amountPx;
|
||||||
|
const pxPerMsInverted = invert ? pxPerMsReadingMode * -1 : pxPerMsReadingMode;
|
||||||
|
|
||||||
|
return { amountPx: pxPerMsInverted, speedMs };
|
||||||
}
|
}
|
||||||
|
|
||||||
return getScrollAmount(amountPerMs, speedMs + 1);
|
return getScrollAmount(amountPerMs, speedMs + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPxPerMs(size: number, scrollAmountPercentage: number, scrollSpeedMs: number) {
|
const getPxPerMs = (size: number, scrollAmountPercentage: number, scrollSpeedMs: number): number =>
|
||||||
return (size * (scrollAmountPercentage / 100)) / scrollSpeedMs;
|
(size * (scrollAmountPercentage / 100)) / scrollSpeedMs;
|
||||||
|
|
||||||
|
function handleScrolling(
|
||||||
|
smooth: boolean,
|
||||||
|
scrollSpeedMs: number,
|
||||||
|
setScrollTriggerId: (id: NodeJS.Timeout | number, type: 'interval' | 'animationFrame') => void,
|
||||||
|
performScroll: (elapsedTime: number) => void,
|
||||||
|
clearScrollTriggers: () => void,
|
||||||
|
): void {
|
||||||
|
clearScrollTriggers();
|
||||||
|
|
||||||
|
if (!smooth) {
|
||||||
|
setScrollTriggerId(
|
||||||
|
setInterval(() => {
|
||||||
|
performScroll(scrollSpeedMs);
|
||||||
|
}, scrollSpeedMs),
|
||||||
|
'interval',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startTime: number;
|
||||||
|
const triggerScroll = (timestamp: DOMHighResTimeStamp) => {
|
||||||
|
const elapsedTime = timestamp - (startTime ?? timestamp);
|
||||||
|
startTime = timestamp;
|
||||||
|
|
||||||
|
performScroll(elapsedTime);
|
||||||
|
setScrollTriggerId(requestAnimationFrame(triggerScroll), 'animationFrame');
|
||||||
|
};
|
||||||
|
|
||||||
|
setScrollTriggerId(requestAnimationFrame(triggerScroll), 'animationFrame');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAutomaticScrolling = (
|
export const useAutomaticScrolling = (
|
||||||
@@ -46,6 +83,7 @@ export const useAutomaticScrolling = (
|
|||||||
|
|
||||||
const elementStyle = useRef<CSSStyleDeclaration>();
|
const elementStyle = useRef<CSSStyleDeclaration>();
|
||||||
const scrollTriggerTimer = useRef<NodeJS.Timeout>();
|
const scrollTriggerTimer = useRef<NodeJS.Timeout>();
|
||||||
|
const scrollTriggerAnimationFrameId = useRef(-1);
|
||||||
|
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
@@ -64,13 +102,18 @@ export const useAutomaticScrolling = (
|
|||||||
}, []),
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const clearScrollTriggers = useCallback(() => {
|
||||||
|
clearTimeout(scrollTriggerTimer.current);
|
||||||
|
cancelAnimationFrame(scrollTriggerAnimationFrameId.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const start = useCallback(() => {
|
const start = useCallback(() => {
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
}, []);
|
}, []);
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
clearInterval(scrollTriggerTimer.current);
|
clearScrollTriggers();
|
||||||
}, []);
|
}, []);
|
||||||
const toggleActive = useCallback(() => {
|
const toggleActive = useCallback(() => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
@@ -88,28 +131,28 @@ export const useAutomaticScrolling = (
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!refOrCallback) {
|
if (!refOrCallback || !isActive) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isActive) {
|
if (isPaused) {
|
||||||
|
clearScrollTriggers();
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollSpeedMs = scrollPerSecond * 1000;
|
const scrollSpeedMs = scrollPerSecond * 1000;
|
||||||
|
|
||||||
const startScrolling = (callback: () => void, timeout: number) => {
|
|
||||||
clearInterval(scrollTriggerTimer.current);
|
|
||||||
scrollTriggerTimer.current = setInterval(() => {
|
|
||||||
if (isActive && !isPaused) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}, timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isCallback) {
|
if (isCallback) {
|
||||||
startScrolling(refOrCallback, scrollSpeedMs);
|
handleScrolling(
|
||||||
return () => clearInterval(scrollTriggerTimer.current);
|
false,
|
||||||
|
scrollSpeedMs,
|
||||||
|
(id) => {
|
||||||
|
scrollTriggerTimer.current = id as NodeJS.Timeout;
|
||||||
|
},
|
||||||
|
refOrCallback,
|
||||||
|
clearScrollTriggers,
|
||||||
|
);
|
||||||
|
return () => clearScrollTriggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = refOrCallback.current;
|
const element = refOrCallback.current;
|
||||||
@@ -128,29 +171,33 @@ export const useAutomaticScrolling = (
|
|||||||
const pxPerMsX = getPxPerMs(window.innerWidth, scrollAmountPercentage, scrollSpeedMs);
|
const pxPerMsX = getPxPerMs(window.innerWidth, scrollAmountPercentage, scrollSpeedMs);
|
||||||
const pxPerMsY = getPxPerMs(window.innerHeight, scrollAmountPercentage, scrollSpeedMs);
|
const pxPerMsY = getPxPerMs(window.innerHeight, scrollAmountPercentage, scrollSpeedMs);
|
||||||
|
|
||||||
const tmpScrollSpeedMs = smooth ? 1 : scrollSpeedMs;
|
handleScrolling(
|
||||||
const { amountPx: pxScrollAmountX, speedMs: timeoutMsX } = getScrollAmount(pxPerMsX, tmpScrollSpeedMs);
|
smooth,
|
||||||
const { amountPx: pxScrollAmountY, speedMs: timeoutMsY } = getScrollAmount(pxPerMsY, tmpScrollSpeedMs);
|
scrollSpeedMs,
|
||||||
|
(id, type) => {
|
||||||
const timeoutMs = handleScrollX ? timeoutMsX : timeoutMsY;
|
switch (type) {
|
||||||
|
case 'interval':
|
||||||
const scrollTopByBase = handleScrollY ? pxScrollAmountY : 0;
|
scrollTriggerTimer.current = id as NodeJS.Timeout;
|
||||||
const scrollLeftByBase = handleScrollX ? pxScrollAmountX : 0;
|
break;
|
||||||
const scrollTopByReadingMode = scrollTopByBase;
|
case 'animationFrame':
|
||||||
const scrollLeftByReadingMode = isRTL ? -scrollLeftByBase : scrollLeftByBase;
|
scrollTriggerAnimationFrameId.current = id as number;
|
||||||
const scrollTopByInverted = invert ? scrollTopByReadingMode * -1 : scrollTopByReadingMode;
|
break;
|
||||||
const scrollLeftByInverted = invert ? scrollLeftByReadingMode * -1 : scrollLeftByReadingMode;
|
default:
|
||||||
|
throw new Error(`Unexpected "type" (${type})`);
|
||||||
startScrolling(() => {
|
}
|
||||||
|
},
|
||||||
|
(elapsedTime) => {
|
||||||
element.scrollBy({
|
element.scrollBy({
|
||||||
top: scrollTopByInverted,
|
top: Number(handleScrollY) * getScrollAmount(pxPerMsY, elapsedTime, false, invert),
|
||||||
left: scrollLeftByInverted,
|
left: Number(handleScrollX) * getScrollAmount(pxPerMsX, elapsedTime, isRTL, invert),
|
||||||
// arg "smooth" triggers the interval so fast that using "behavior smooth" doesn't look smooth and also slows down the scrolling
|
// arg "smooth" triggers the interval so fast that using "behavior smooth" doesn't look smooth and also slows down the scrolling
|
||||||
behavior: smooth ? undefined : 'smooth',
|
behavior: smooth ? undefined : 'smooth',
|
||||||
});
|
});
|
||||||
}, timeoutMs);
|
},
|
||||||
|
clearScrollTriggers,
|
||||||
|
);
|
||||||
|
|
||||||
return () => clearInterval(scrollTriggerTimer.current);
|
return () => clearScrollTriggers();
|
||||||
}, [refOrCallback, scrollPerSecond, scrollAmountPercentage, screenDimensions, isActive, isPaused, invert, smooth]);
|
}, [refOrCallback, scrollPerSecond, scrollAmountPercentage, screenDimensions, isActive, isPaused, invert, smooth]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
|||||||
Reference in New Issue
Block a user