Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/raf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"primitives"
],
"dependencies": {
"@solid-primitives/rootless": "workspace:^",
"@solid-primitives/set": "workspace:^",
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
Expand Down
126 changes: 122 additions & 4 deletions packages/raf/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type MaybeAccessor, noop } from "@solid-primitives/utils";
import { createHydratableSingletonRoot } from "@solid-primitives/rootless";
import { ReactiveSet } from "@solid-primitives/set";
import { access, type MaybeAccessor, noop } from "@solid-primitives/utils";
import { createSignal, createMemo, type Accessor, onCleanup } from "solid-js";
import { isServer } from "solid-js/web";

Expand All @@ -23,7 +25,7 @@ function createRAF(
return [() => false, noop, noop];
}
const [running, setRunning] = createSignal(false);
let requestID = 0;
let requestID: number | null = null;

const loop: FrameRequestCallback = timeStamp => {
requestID = requestAnimationFrame(loop);
Expand All @@ -36,7 +38,116 @@ function createRAF(
};
const stop = () => {
setRunning(false);
cancelAnimationFrame(requestID);
if (requestID !== null) cancelAnimationFrame(requestID);
};

onCleanup(stop);
return [running, start, stop];
}

/**
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation is way too complex. "A version of createRAF that batches multiple frames within the same render cycle instead of skipping them ."

*
* This is a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) primitive.
*
* @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.
* ```ts
* (callback: FrameRequestCallback, automatic?: boolean) => [queued: Accessor<boolean>, queue: VoidFunction, dequeue: VoidFunction, running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
* ```
*
* @example
* const createScheduledFrame = useFrameloop();
*
* const [queued, queue, dequeue, running, start, stop] = createScheduledFrame(() => {
* el.style.transform = "translateX(...)"
* });
*/
const useFrameloop = createHydratableSingletonRoot<
(
callback: FrameRequestCallback,
automatic?: MaybeAccessor<boolean>,
) => [
queued: Accessor<boolean>,
queue: VoidFunction,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I generally dislike this addition, but why not extract the queue logic into a separate primitive that can also be used with the normal createRAF?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could I get some guidance on how that would be structured? Do you propose adding the queue logic as a separate create helper?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like

function queueCallbacks(
  ...initialCallbacks: (() => void)[]
): [callback: () => void, { add: (cb) => void, del: (cb) => void }] {
  ...
}

dequeue: VoidFunction,
running: Accessor<boolean>,
start: VoidFunction,
stop: VoidFunction,
]
>(() => {
if (isServer) return () => [() => false, noop, noop, () => false, noop, noop];

const frameCallbacks = new ReactiveSet<FrameRequestCallback>();

const [running, start, stop] = createRAF(delta => {
frameCallbacks.forEach(frameCallback => {
frameCallback(delta);
});
});

return function createFrame(callback: FrameRequestCallback, automatic = false) {
const queued = () => frameCallbacks.has(callback);
const queue = () => {
frameCallbacks.add(callback);
if (access(automatic) && !running()) start();
};
const dequeue = () => {
frameCallbacks.delete(callback);
if (running() && frameCallbacks.size === 0) stop();
};

onCleanup(dequeue);
return [queued, queue, dequeue, running, start, stop];
};
});

/**
* An advanced primitive creating reactive scheduled frameloops, for example [motion's frame util](https://motion.dev/docs/frame), that are automatically disposed onCleanup.
*
* 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`.
*
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createScheduledFrameloop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You seem to have forgotten to update the README.md

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry for that!

* @param schedule The function that receives the callback and handles scheduling the frameloop
* @param cancel The function that cancels the scheduled callback
* @param callback The callback to run each scheduled frame
* @returns Returns a signal if currently running as well as start and stop methods
* ```ts
* [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
* ```
*
* @example
* import { type FrameData, cancelFrame, frame } from "motion";
*
* const [running, start, stop] = createScheduledFrameloop(
* callback => frame.update(callback, true),
* cancelFrame,
* (data: FrameData) => {
* // Do something with the data.delta during the `update` phase.
* },
* );
*/
function createScheduledFrameloop<
RequestID extends NonNullable<unknown>,
Callback extends (...args: Array<any>) => any,
>(
schedule: (callback: Callback) => RequestID,
cancel: (requestID: RequestID) => void,
callback: Callback,
): [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction] {
if (isServer) {
return [() => false, noop, noop];
}
const [running, setRunning] = createSignal(false);
let requestID: RequestID | null = null;

const start = () => {
if (running()) return;
setRunning(true);
requestID = schedule(callback);
};
const stop = () => {
setRunning(false);
if (requestID !== null) cancel(requestID);
};

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

export { createMs, createRAF, createRAF as default, targetFPS };
export {
createMs,
createRAF,
createRAF as default,
createScheduledFrameloop,
targetFPS,
useFrameloop,
};
Loading