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
123 changes: 123 additions & 0 deletions packages/raf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,129 @@ function createMs(
};
```

## createCallbacksSet

A primitive for executing multiple callbacks at once, intended for usage in conjunction with primitives like `createRAF`, where you want to execute multiple callbacks in the same `window.requestAnimationFrame` (sharing the timestamp).

#### Definition

```ts
function createCallbacksSet<T extends (...args: any) => void>(...initialCallbacks: Array<T>): [callback: T, callbacksSet: ReactiveSet<T>]
```

## useGlobalRAF

A singleton root that returns a function similar to `createRAF` that batches multiple `window.requestAnimationFrame` executions within the same same timestamp (same RAF cycle) instead of skipping requests in separate frames. This is done by using a single `createRAF` in a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) in conjuction with [`createCallbacksSet`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createCallbacksSet)

Returns a factory function that works like `createRAF` with an additional parameter to start the global RAF loop when adding the callback to the callbacks set. This function return is also similar to `createRAF`, but it's first three elements of the tuple are related to the presence of the callback in the callbacks set, while the next three are the same as `createRAF`, but for the global loop that executes all the callbacks present in the callbacks set.

```ts
import { useGlobalRAF } from "@solid-primitives/raf";

const createScheduledLoop = useGlobalRAF()
const [hasAddedManual, addManual, removeManual, isRunningManual] = createScheduledLoop(
timeStamp => console.log("Time stamp is", timeStamp)
);
const [hasAddedAuto, addAuto, removeAuto, isRunningAuto] = createScheduledLoop(
timeStamp => console.log("Time stamp is", timeStamp),
true
);

hasAddedManual() // false
addManual()
hasAddedManual() // true
isRunningManual() // false

hasAddedAuto() // false
addAuto()
hasAddedAuto() // true
// Both are running on the same global loop
isRunningAuto() // true
isRunningManual() // true
```

#### Example

```ts
import { targetFPS, useGlobalRAF } from "@solid-primitives/raf";

const createScheduledLoop = useGlobalRAF()

const [hasAddedLowFramerate, addLowFramerate, removeLowFramerate] = createScheduledLoop(
targetFPS(
() => {
/* Low framerate loop, for example for video / webcam sampling where the framerate can be capped by external sources */
},
30
),
true
);
const [hasAddedHighFramerate, addHighFramerate, removeHighFramerate] = createScheduledLoop(
targetFPS(
() => {
/* High framerate loop for an animation / drawing to a canvas */
},
60
),
true
);
```

#### Definition

```ts
function useGlobalRAF(): (callback: FrameRequestCallback, startWhenAdded?: MaybeAccessor<boolean>) => [
added: Accessor<boolean>,
add: VoidFunction,
remove: VoidFunction,
running: Accessor<boolean>,
start: VoidFunction,
stop: VoidFunction
];
```

#### Warning

Only use this when you absolutely need to schedule animations on the same frame and stick to quick executions trying not to overload the amount of work performed in the current animation frame. If you need to ensure multiple things run on the same request but you also want to schedule multiple requests, you can use [`createCallbacksSet`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createCallbacksSet) and a singleton `createRAF` to compose something similar to this primitive.

## createScheduledLoop

A primitive for creating reactive interactions with external frameloop related functions (for example using [motion's frame util](https://motion.dev/docs/frame)) that are automatically disposed onCleanup.

```ts
import { cancelFrame, frame } from "motion";

const createMotionFrameRender = createScheduledLoop(
callback => frame.render(callback, true),
cancelFrame,
);
const [running, start, stop] = createMotionFrameRender(
data => element.style.transform = "translateX(...)"
);

// Alternative syntax (for a single execution in place):
import { cancelFrame, frame } from "motion";

const [running, start, stop] = createScheduledLoop(
callback => frame.render(callback, true),
cancelFrame,
)(
data => element.style.transform = "translateX(...)"
);
```

#### Definition

```ts
function createScheduledLoop<
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]
```

## Demo

You may view a working example here: https://codesandbox.io/s/solid-primitives-raf-demo-4xvmjd?file=/src/index.tsx
Expand Down
2 changes: 2 additions & 0 deletions packages/raf/package.json
Copy link
Member

Choose a reason for hiding this comment

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

You need to add your primitives to primitive.list.

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
149 changes: 145 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,13 +38,144 @@ function createRAF(
};
const stop = () => {
setRunning(false);
cancelAnimationFrame(requestID);
if (requestID !== null) cancelAnimationFrame(requestID);
};

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

/**
* A primitive for executing multiple callbacks at once, intended for usage in conjunction with primitives like `createRAF`.
* @param initialCallbacks
* @returns a main callback function that executes all the callbacks at once, as well as the `ReactiveSet` that contains all the callbacks
* ```ts
* [callback: T, callbacksSet: ReactiveSet<T>]
* ```
*/
function createCallbacksSet<T extends (...args: any) => void>(
...initialCallbacks: Array<T>
): [callback: T, callbacksSet: ReactiveSet<T>] {
const callbacksSet = new ReactiveSet(initialCallbacks);

return [((...args) => callbacksSet.forEach(callback => callback(...args))) as T, callbacksSet];
}

/**
* A singleton root that returns a function similar to `createRAF` that batches multiple `window.requestAnimationFrame` executions within the same same timestamp (same RAF cycle) instead of skipping requests in separate frames. This is done by using a single `createRAF` in a [singleton root](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) in conjuction with [`createCallbacksSet`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createCallbacksSet)
*
* @returns Returns a factory function that works like `createRAF` with an additional parameter to start the global RAF loop when adding the callback to the callbacks set. This function return is also similar to `createRAF`, but it's first three elements of the tuple are related to the presence of the callback in the callbacks set, while the next three are the same as `createRAF`, but for the global loop that executes all the callbacks present in the callbacks set.
* ```ts
* (callback: FrameRequestCallback, startWhenAdded?: boolean) => [added: Accessor<boolean>, add: VoidFunction, remove: VoidFunction, running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
* ```
*
* @example
* const createGlobalRAFCallback = useGlobalRAF();
*
* const [added, add, remove, running, start, stop] = createGlobalRAFCallback(() => {
* el.style.transform = "translateX(...)"
* });
*
* // Usage with targetFPS
* const [added, add, remove, running, start, stop] = createGlobalRAFCallback(targetFPS(() => {
* el.style.transform = "translateX(...)"
* }, 60));
*/
const useGlobalRAF = createHydratableSingletonRoot<
(
callback: FrameRequestCallback,
startWhenAdded?: MaybeAccessor<boolean>,
) => [
added: Accessor<boolean>,
add: VoidFunction,
remove: VoidFunction,
running: Accessor<boolean>,
start: VoidFunction,
stop: VoidFunction,
]
>(() => {
if (isServer) return () => [() => false, noop, noop, () => false, noop, noop];

const [callback, callbacksSet] = createCallbacksSet<FrameRequestCallback>();
const [running, start, stop] = createRAF(callback);

return function createGlobalRAFCallback(callback: FrameRequestCallback, startWhenAdded = false) {
const added = () => callbacksSet.has(callback);
const add = () => {
callbacksSet.add(callback);
if (access(startWhenAdded) && !running()) start();
};
const remove = () => {
callbacksSet.delete(callback);
if (running() && callbacksSet.size === 0) stop();
};

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

/**
* A primitive for creating reactive interactions with external frameloop related functions (for example using [motion's frame util](https://motion.dev/docs/frame)) that are automatically disposed onCleanup.
*
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createScheduledLoop
* @param schedule The function that receives the callback and handles it's loop scheduling, returning a requestID that is used to cancel the loop
* @param cancel The function that cancels the scheduled callback using the requestID.
* @returns Returns a function that receives a callback that's compatible with the provided scheduler and returns a signal if currently running as well as start and stop methods
* ```ts
* (callback: Callback) => [running: Accessor<boolean>, start: VoidFunction, stop: VoidFunction]
* ```
*
* @example
* import { cancelFrame, frame } from "motion";
*
* const createMotionFrameRender = createScheduledLoop(
* callback => frame.render(callback, true),
* cancelFrame,
* );
* const [running, start, stop] = createMotionFrameRender(
* data => element.style.transform = "translateX(...)"
* );
*
* // Alternative syntax (for a single execution in place):
* import { cancelFrame, frame } from "motion";
*
* const [running, start, stop] = createScheduledLoop(
* callback => frame.render(callback, true),
* cancelFrame,
* )(
* data => element.style.transform = "translateX(...)"
* );
*/
function createScheduledLoop<
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] {
return (callback: Callback) => {
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);
return [running, start, stop];
};
}

/**
* A primitive for wrapping `window.requestAnimationFrame` callback function to limit the execution of the callback to specified number of FPS.
*
Expand Down Expand Up @@ -131,4 +264,12 @@ 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,
createCallbacksSet,
createRAF,
createRAF as default,
createScheduledLoop,
targetFPS,
useGlobalRAF,
};
Loading