Skip to content

Commit d005f2e

Browse files
committed
feat(raf): add frameloop utils.
- Added `useFrameloop` util to use unified request animation frame calls. - Added `createScheduledFrameloop` to handle request animation frame from external sources. - Fixed `createRAF` cleanup for id `0` by using `null` instead.
1 parent 0cbdb59 commit d005f2e

File tree

5 files changed

+414
-6
lines changed

5 files changed

+414
-6
lines changed

packages/raf/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
"primitives"
5555
],
5656
"dependencies": {
57+
"@solid-primitives/rootless": "workspace:^",
58+
"@solid-primitives/set": "workspace:^",
5759
"@solid-primitives/utils": "workspace:^"
5860
},
5961
"peerDependencies": {

packages/raf/src/index.ts

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { type MaybeAccessor, noop } from "@solid-primitives/utils";
1+
import { createHydratableSingletonRoot } from "@solid-primitives/rootless";
2+
import { ReactiveSet } from "@solid-primitives/set";
3+
import { access, type MaybeAccessor, noop } from "@solid-primitives/utils";
24
import { createSignal, createMemo, type Accessor, onCleanup } from "solid-js";
35
import { isServer } from "solid-js/web";
46

@@ -23,7 +25,7 @@ function createRAF(
2325
return [() => false, noop, noop];
2426
}
2527
const [running, setRunning] = createSignal(false);
26-
let requestID = 0;
28+
let requestID: number | null = null;
2729

2830
const loop: FrameRequestCallback = timeStamp => {
2931
requestID = requestAnimationFrame(loop);
@@ -36,7 +38,116 @@ function createRAF(
3638
};
3739
const stop = () => {
3840
setRunning(false);
39-
cancelAnimationFrame(requestID);
41+
if (requestID !== null) cancelAnimationFrame(requestID);
42+
};
43+
44+
onCleanup(stop);
45+
return [running, start, stop];
46+
}
47+
48+
/**
49+
* Returns an advanced primitive factory function (that has an API similar to `createRAF`) to handle multiple animation frame callbacks in a single batched `requestAnimationFrame`, avoiding the overhead of scheduling multiple animation frames outside of a batch and making them all sync on the same delta.
50+
*
51+
* This is a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) primitive.
52+
*
53+
* @returns Returns a factory function that works like `createRAF` but handles all scheduling in the same frame batch and optionally automatically starts and stops the global loop.
54+
* ```ts
55+
* (callback: FrameRequestCallback, automatic?: boolean) => [queued: Accessor<boolean>, queue: VoidFunction, dequeue: VoidFunction, running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
56+
* ```
57+
*
58+
* @example
59+
* const createScheduledFrame = useFrameloop();
60+
*
61+
* const [queued, queue, dequeue, running, start, stop] = createScheduledFrame(() => {
62+
* el.style.transform = "translateX(...)"
63+
* });
64+
*/
65+
const useFrameloop = createHydratableSingletonRoot<
66+
(
67+
callback: FrameRequestCallback,
68+
automatic?: MaybeAccessor<boolean>,
69+
) => [
70+
queued: Accessor<boolean>,
71+
queue: VoidFunction,
72+
dequeue: VoidFunction,
73+
running: Accessor<boolean>,
74+
start: VoidFunction,
75+
stop: VoidFunction,
76+
]
77+
>(() => {
78+
if (isServer) return () => [() => false, noop, noop, () => false, noop, noop];
79+
80+
const frameCallbacks = new ReactiveSet<FrameRequestCallback>();
81+
82+
const [running, start, stop] = createRAF(delta => {
83+
frameCallbacks.forEach(frameCallback => {
84+
frameCallback(delta);
85+
});
86+
});
87+
88+
return function createFrame(callback: FrameRequestCallback, automatic = false) {
89+
const queued = () => frameCallbacks.has(callback);
90+
const queue = () => {
91+
frameCallbacks.add(callback);
92+
if (access(automatic) && !running()) start();
93+
};
94+
const dequeue = () => {
95+
frameCallbacks.delete(callback);
96+
if (running() && frameCallbacks.size === 0) stop();
97+
};
98+
99+
onCleanup(dequeue);
100+
return [queued, queue, dequeue, running, start, stop];
101+
};
102+
});
103+
104+
/**
105+
* An advanced primitive creating reactive scheduled frameloops, for example [motion's frame util](https://motion.dev/docs/frame), that are automatically disposed onCleanup.
106+
*
107+
* The idea behind this is for more complex use cases, where you need scheduling and want to avoid potential issues arising from running more than one `requestAnimationFrame`.
108+
*
109+
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createScheduledFrameloop
110+
* @param schedule The function that receives the callback and handles scheduling the frameloop
111+
* @param cancel The function that cancels the scheduled callback
112+
* @param callback The callback to run each scheduled frame
113+
* @returns Returns a signal if currently running as well as start and stop methods
114+
* ```ts
115+
* [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
116+
* ```
117+
*
118+
* @example
119+
* import { type FrameData, cancelFrame, frame } from "motion";
120+
*
121+
* const [running, start, stop] = createScheduledFrameloop(
122+
* callback => frame.update(callback, true),
123+
* cancelFrame,
124+
* (data: FrameData) => {
125+
* // Do something with the data.delta during the `update` phase.
126+
* },
127+
* );
128+
*/
129+
function createScheduledFrameloop<
130+
RequestID extends NonNullable<unknown>,
131+
Callback extends (...args: Array<any>) => any,
132+
>(
133+
schedule: (callback: Callback) => RequestID,
134+
cancel: (requestID: RequestID) => void,
135+
callback: Callback,
136+
): [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction] {
137+
if (isServer) {
138+
return [() => false, noop, noop];
139+
}
140+
const [running, setRunning] = createSignal(false);
141+
let requestID: RequestID | null = null;
142+
143+
const start = () => {
144+
if (running()) return;
145+
setRunning(true);
146+
requestID = schedule(callback);
147+
};
148+
const stop = () => {
149+
setRunning(false);
150+
if (requestID !== null) cancel(requestID);
40151
};
41152

42153
onCleanup(stop);
@@ -131,4 +242,11 @@ function createMs(fps: MaybeAccessor<number>, limit?: MaybeAccessor<number>): Ms
131242
return Object.assign(ms, { reset, running, start, stop });
132243
}
133244

134-
export { createMs, createRAF, createRAF as default, targetFPS };
245+
export {
246+
createMs,
247+
createRAF,
248+
createRAF as default,
249+
createScheduledFrameloop,
250+
targetFPS,
251+
useFrameloop,
252+
};

0 commit comments

Comments
 (0)