feat: add deterministic scroll drive controller
This commit is contained in:
87
src/composables/useScrollDrive.ts
Normal file
87
src/composables/useScrollDrive.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { onMounted, onUnmounted, readonly, shallowRef, type Ref } from 'vue';
|
||||
import { createScrollDriveController } from '@/journey/scroll-drive-controller';
|
||||
import type { ScrollDriveSample, ScrollDriveSnapshot } from '@/journey/types';
|
||||
|
||||
const PASSIVE_LISTENER: AddEventListenerOptions = { passive: true };
|
||||
|
||||
const initialSnapshot = (): ScrollDriveSnapshot => ({
|
||||
scrollY: 0,
|
||||
routeProgress: 0,
|
||||
direction: 0,
|
||||
acceleration: 0,
|
||||
driveSpeed: 0.18,
|
||||
});
|
||||
|
||||
export function useScrollDrive(): Readonly<Ref<ScrollDriveSnapshot>> {
|
||||
const controller = createScrollDriveController();
|
||||
const snapshot = shallowRef<ScrollDriveSnapshot>(initialSnapshot());
|
||||
let teardown = (): void => {};
|
||||
|
||||
onMounted(() => {
|
||||
let frameId: number | undefined;
|
||||
let resetOnNextFrame = false;
|
||||
const visualViewport = window.visualViewport;
|
||||
|
||||
const readSample = (now: number): ScrollDriveSample => {
|
||||
const scrollingElement = document.scrollingElement ?? document.documentElement;
|
||||
const viewportHeight = visualViewport?.height ?? document.documentElement.clientHeight;
|
||||
return {
|
||||
scrollY: scrollingElement.scrollTop,
|
||||
now,
|
||||
maxScrollY: Math.max(0, scrollingElement.scrollHeight - viewportHeight),
|
||||
};
|
||||
};
|
||||
|
||||
const runFrame = (now: number): void => {
|
||||
frameId = undefined;
|
||||
const input = readSample(now);
|
||||
snapshot.value = resetOnNextFrame
|
||||
? controller.reset(input)
|
||||
: controller.sample(input);
|
||||
resetOnNextFrame = false;
|
||||
};
|
||||
|
||||
const scheduleFrame = (): void => {
|
||||
if (frameId === undefined && document.visibilityState !== 'hidden') {
|
||||
frameId = requestAnimationFrame(runFrame);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = (): void => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
resetOnNextFrame = true;
|
||||
if (frameId !== undefined) {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = undefined;
|
||||
}
|
||||
snapshot.value = controller.reset();
|
||||
return;
|
||||
}
|
||||
resetOnNextFrame = true;
|
||||
scheduleFrame();
|
||||
};
|
||||
|
||||
snapshot.value = controller.reset(readSample(performance.now()));
|
||||
window.addEventListener('scroll', scheduleFrame, PASSIVE_LISTENER);
|
||||
window.addEventListener('resize', scheduleFrame, PASSIVE_LISTENER);
|
||||
visualViewport?.addEventListener('resize', scheduleFrame, PASSIVE_LISTENER);
|
||||
visualViewport?.addEventListener('scroll', scheduleFrame, PASSIVE_LISTENER);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange, PASSIVE_LISTENER);
|
||||
|
||||
teardown = (): void => {
|
||||
window.removeEventListener('scroll', scheduleFrame, PASSIVE_LISTENER);
|
||||
window.removeEventListener('resize', scheduleFrame, PASSIVE_LISTENER);
|
||||
visualViewport?.removeEventListener('resize', scheduleFrame, PASSIVE_LISTENER);
|
||||
visualViewport?.removeEventListener('scroll', scheduleFrame, PASSIVE_LISTENER);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange, PASSIVE_LISTENER);
|
||||
if (frameId !== undefined) {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = undefined;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => teardown());
|
||||
|
||||
return readonly(snapshot);
|
||||
}
|
||||
Reference in New Issue
Block a user