-
Notifications
You must be signed in to change notification settings - Fork 154
feat(raf): add frameloop utils. #824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d005f2e
1a8b2e8
95c5838
74ab832
ff3888b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"; | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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]; | ||
| }; | ||
| } | ||
|
Comment on lines
+150
to
+177
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: The frame.render function (part of Motion's frameloop) does not return a value—specifically, it returns undefined [1]. When using cancelFrame, you must provide the original callback reference (the function you passed to frame.render) [1][2][3]. You cannot use a return value, as the function does not provide one [1]. In practice, frame.render is used to schedule a task to run during the render phase of Motion's animation loop [4][5]. Because it is a scheduling utility rather than an execution wrapper with a return handle, cancellation relies entirely on passing the exact function reference that was initially scheduled so it can be identified and removed from the internal queue [1]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the relevant source and tests.
git ls-files 'packages/raf/src/index.ts' 'test/index.test.ts' 'package.json' 'pnpm-lock.yaml' 'package-lock.json' 'yarn.lock' | sed 's/^/FILE /'
echo
echo '--- packages/raf/src/index.ts (selected range) ---'
sed -n '1,240p' packages/raf/src/index.ts | cat -n
echo
echo '--- test/index.test.ts (selected range) ---'
sed -n '1,140p' test/index.test.ts | cat -n
echo
echo '--- search for Motion usage and createScheduledLoop/createRAF ---'
rg -n "createScheduledLoop|createRAF|frame\.render|cancelFrame|requestAnimationFrame" packages test . -g '!**/dist/**' -g '!**/build/**'Repository: solidjs-community/solid-primitives Length of output: 11284 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- repo files matching raf tests ---'
fd -a 'index.test.ts|raf.*test.*|test.*ts' . packages test
echo
echo '--- package.json ---'
sed -n '1,240p' package.json | cat -n
echo
echo '--- package-specific package.json files ---'
fd -a 'package.json' packages | while read -r f; do
echo "FILE $f"
sed -n '1,220p' "$f" | rg -n '"(motion|`@motionone`|framer-motion|solid-js)"'
done
echo
echo '--- search for createScheduledLoop tests/usages ---'
rg -n "createScheduledLoop|frame\.render|cancelFrame|targetFPS|createRAF" packages . -g '!**/dist/**' -g '!**/build/**'Repository: solidjs-community/solid-primitives Length of output: 50390 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- packages/raf/test/index.test.ts (relevant section) ---'
sed -n '1,120p' packages/raf/test/index.test.ts | cat -n
echo
echo '--- packages/raf/README.md (createScheduledLoop section) ---'
sed -n '200,245p' packages/raf/README.md | cat -n
echo
echo '--- motion dependency presence in lockfile/package ---'
rg -n '"motion"|from "motion"|from '\''motion'\''|cancelFrame|frame\.render' package.json packages/raf/package.json pnpm-lock.yaml packages/raf/README.md packages/raf/src/index.tsRepository: solidjs-community/solid-primitives Length of output: 8657 Fix the Motion example’s cancellation token 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * A primitive for wrapping `window.requestAnimationFrame` callback function to limit the execution of the callback to specified number of FPS. | ||
| * | ||
|
|
@@ -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, | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.