feat: drive signs from a shared animation clock

This commit is contained in:
2026-09-02 14:14:18 +03:00
parent 24afda8710
commit f2dd3f85dd
9 changed files with 615 additions and 265 deletions

View File

@@ -12,9 +12,23 @@ const initialSnapshot = (): ScrollDriveSnapshot => ({
driveSpeed: 0.18,
});
export function useScrollDrive(): Readonly<Ref<ScrollDriveSnapshot>> {
export interface ScrollDriveBinding {
snapshot: Readonly<Ref<ScrollDriveSnapshot>>;
requestFrame(): void;
subscribeFrame(
listener: (now: number, snapshot: ScrollDriveSnapshot) => boolean,
): () => void;
}
export function useScrollDrive(): ScrollDriveBinding {
const controller = createScrollDriveController();
const snapshot = shallowRef<ScrollDriveSnapshot>(initialSnapshot());
const subscribers = new Set<(
now: number,
snapshot: ScrollDriveSnapshot,
) => boolean>();
let requestFrame = (): void => {};
let disposed = false;
let teardown = (): void => {};
onMounted(() => {
@@ -32,21 +46,29 @@ export function useScrollDrive(): Readonly<Ref<ScrollDriveSnapshot>> {
};
};
let scheduleFrame = (): void => {};
const runFrame = (now: number): void => {
frameId = undefined;
const input = readSample(now);
snapshot.value = resetOnNextFrame
const nextSnapshot = resetOnNextFrame
? controller.reset(input)
: controller.sample(input);
snapshot.value = nextSnapshot;
resetOnNextFrame = false;
if (snapshot.value.acceleration > 0) scheduleFrame();
let keepRunning = false;
for (const subscriber of [...subscribers]) {
keepRunning = subscriber(now, nextSnapshot) || keepRunning;
}
if (nextSnapshot.acceleration > 0 || keepRunning) scheduleFrame();
};
const scheduleFrame = (): void => {
scheduleFrame = (): void => {
if (frameId === undefined && document.visibilityState !== 'hidden') {
frameId = requestAnimationFrame(runFrame);
}
};
requestFrame = scheduleFrame;
const handleVisibilityChange = (): void => {
if (document.visibilityState === 'hidden') {
@@ -70,6 +92,8 @@ export function useScrollDrive(): Readonly<Ref<ScrollDriveSnapshot>> {
document.addEventListener('visibilitychange', handleVisibilityChange, PASSIVE_LISTENER);
teardown = (): void => {
disposed = true;
requestFrame = (): void => {};
window.removeEventListener('scroll', scheduleFrame, PASSIVE_LISTENER);
window.removeEventListener('resize', scheduleFrame, PASSIVE_LISTENER);
visualViewport?.removeEventListener('resize', scheduleFrame, PASSIVE_LISTENER);
@@ -79,10 +103,19 @@ export function useScrollDrive(): Readonly<Ref<ScrollDriveSnapshot>> {
cancelAnimationFrame(frameId);
frameId = undefined;
}
subscribers.clear();
};
});
onUnmounted(() => teardown());
return readonly(snapshot);
return {
snapshot: readonly(snapshot),
requestFrame: () => requestFrame(),
subscribeFrame(listener) {
if (disposed) return () => {};
subscribers.add(listener);
return () => subscribers.delete(listener);
},
};
}