@motion-core/motion-gpu 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -1
- package/dist/core/material-preprocess.d.ts +5 -5
- package/dist/core/material-preprocess.js +1 -4
- package/dist/core/material.d.ts +32 -23
- package/dist/core/material.js +14 -7
- package/dist/core/types.d.ts +20 -10
- package/dist/react/FragCanvas.d.ts +26 -0
- package/dist/react/FragCanvas.js +218 -0
- package/dist/react/FragCanvas.tsx +345 -0
- package/dist/react/MotionGPUErrorOverlay.d.ts +6 -0
- package/dist/react/MotionGPUErrorOverlay.js +52 -0
- package/dist/react/MotionGPUErrorOverlay.tsx +129 -0
- package/dist/react/Portal.d.ts +6 -0
- package/dist/react/Portal.js +24 -0
- package/dist/react/Portal.tsx +34 -0
- package/dist/react/advanced.d.ts +11 -0
- package/dist/react/advanced.js +6 -0
- package/dist/react/frame-context.d.ts +14 -0
- package/dist/react/frame-context.js +98 -0
- package/dist/react/index.d.ts +15 -0
- package/dist/react/index.js +9 -0
- package/dist/react/motiongpu-context.d.ts +73 -0
- package/dist/react/motiongpu-context.js +18 -0
- package/dist/react/use-motiongpu-user-context.d.ts +49 -0
- package/dist/react/use-motiongpu-user-context.js +94 -0
- package/dist/react/use-texture.d.ts +40 -0
- package/dist/react/use-texture.js +162 -0
- package/dist/svelte/FragCanvas.svelte +8 -22
- package/dist/svelte/Portal.svelte +6 -21
- package/dist/svelte/use-motiongpu-user-context.d.ts +9 -1
- package/dist/svelte/use-motiongpu-user-context.js +4 -1
- package/dist/svelte/use-texture.d.ts +6 -2
- package/dist/svelte/use-texture.js +6 -2
- package/package.json +28 -3
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useRef } from 'react';
|
|
2
|
+
import { createCurrentWritable } from '../core/current-value.js';
|
|
3
|
+
import { useMotionGPU } from './motiongpu-context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Placeholder stage used before a frame task registration becomes available.
|
|
6
|
+
*/
|
|
7
|
+
const PENDING_STAGE_KEY = Symbol('motiongpu-react-pending-stage');
|
|
8
|
+
/**
|
|
9
|
+
* React context container for the active frame registry.
|
|
10
|
+
*/
|
|
11
|
+
export const FrameRegistryReactContext = createContext(null);
|
|
12
|
+
/**
|
|
13
|
+
* Registers a callback in the active frame registry and auto-unsubscribes on unmount.
|
|
14
|
+
*
|
|
15
|
+
* @param keyOrCallback - Task key or callback for auto-key registration.
|
|
16
|
+
* @param callbackOrOptions - Callback (keyed overload) or options (auto-key overload).
|
|
17
|
+
* @param maybeOptions - Optional registration options for keyed overload.
|
|
18
|
+
* Registration key/options are frozen on first render; subsequent renders do not re-register.
|
|
19
|
+
* @returns Registration control API with task, start/stop controls and started state.
|
|
20
|
+
* @throws {Error} When called outside `<FragCanvas>`.
|
|
21
|
+
* @throws {Error} When callback is missing in keyed overload.
|
|
22
|
+
*/
|
|
23
|
+
export function useFrame(keyOrCallback, callbackOrOptions, maybeOptions) {
|
|
24
|
+
const registry = useContext(FrameRegistryReactContext);
|
|
25
|
+
if (!registry) {
|
|
26
|
+
throw new Error('useFrame must be used inside <FragCanvas>');
|
|
27
|
+
}
|
|
28
|
+
const motiongpu = useMotionGPU();
|
|
29
|
+
const resolved = typeof keyOrCallback === 'function'
|
|
30
|
+
? {
|
|
31
|
+
key: undefined,
|
|
32
|
+
callback: keyOrCallback,
|
|
33
|
+
options: callbackOrOptions
|
|
34
|
+
}
|
|
35
|
+
: {
|
|
36
|
+
key: keyOrCallback,
|
|
37
|
+
callback: callbackOrOptions,
|
|
38
|
+
options: maybeOptions
|
|
39
|
+
};
|
|
40
|
+
if (typeof resolved.callback !== 'function') {
|
|
41
|
+
throw new Error('useFrame requires a callback');
|
|
42
|
+
}
|
|
43
|
+
const callbackRef = useRef(resolved.callback);
|
|
44
|
+
callbackRef.current = resolved.callback;
|
|
45
|
+
const registrationConfigRef = useRef(null);
|
|
46
|
+
if (!registrationConfigRef.current) {
|
|
47
|
+
registrationConfigRef.current = {
|
|
48
|
+
key: resolved.key,
|
|
49
|
+
options: resolved.options
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const registrationConfig = registrationConfigRef.current;
|
|
53
|
+
const registrationRef = useRef(null);
|
|
54
|
+
const taskRef = useRef({
|
|
55
|
+
key: registrationConfig.key !== undefined
|
|
56
|
+
? registrationConfig.key
|
|
57
|
+
: Symbol('motiongpu-react-pending-task-key'),
|
|
58
|
+
stage: PENDING_STAGE_KEY
|
|
59
|
+
});
|
|
60
|
+
const startedStoreRef = useRef(createCurrentWritable(false));
|
|
61
|
+
const startedStore = startedStoreRef.current;
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const wrappedCallback = (state) => {
|
|
64
|
+
callbackRef.current(state);
|
|
65
|
+
};
|
|
66
|
+
const registration = registrationConfig.key === undefined
|
|
67
|
+
? registry.register(wrappedCallback, registrationConfig.options)
|
|
68
|
+
: registry.register(registrationConfig.key, wrappedCallback, registrationConfig.options);
|
|
69
|
+
registrationRef.current = registration;
|
|
70
|
+
taskRef.current = registration.task;
|
|
71
|
+
const unsubscribeStarted = registration.started.subscribe((value) => {
|
|
72
|
+
startedStore.set(value);
|
|
73
|
+
});
|
|
74
|
+
return () => {
|
|
75
|
+
unsubscribeStarted();
|
|
76
|
+
registration.unsubscribe();
|
|
77
|
+
if (registrationRef.current === registration) {
|
|
78
|
+
registrationRef.current = null;
|
|
79
|
+
}
|
|
80
|
+
startedStore.set(false);
|
|
81
|
+
};
|
|
82
|
+
}, [registrationConfig, registry, startedStore]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
motiongpu.invalidate();
|
|
85
|
+
}, [motiongpu, resolved.callback]);
|
|
86
|
+
return {
|
|
87
|
+
get task() {
|
|
88
|
+
return taskRef.current;
|
|
89
|
+
},
|
|
90
|
+
start: () => {
|
|
91
|
+
registrationRef.current?.start();
|
|
92
|
+
},
|
|
93
|
+
stop: () => {
|
|
94
|
+
registrationRef.current?.stop();
|
|
95
|
+
},
|
|
96
|
+
started: startedStore
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React adapter entrypoint for MotionGPU.
|
|
3
|
+
*/
|
|
4
|
+
export { FragCanvas } from './FragCanvas.js';
|
|
5
|
+
export { defineMaterial } from '../core/material.js';
|
|
6
|
+
export { BlitPass, CopyPass, ShaderPass } from '../passes/index.js';
|
|
7
|
+
export { useMotionGPU } from './motiongpu-context.js';
|
|
8
|
+
export { useFrame } from './frame-context.js';
|
|
9
|
+
export { useTexture } from './use-texture.js';
|
|
10
|
+
export type { FrameInvalidationToken, FrameState, OutputColorSpace, RenderPass, RenderPassContext, RenderPassFlags, RenderPassInputSlot, RenderPassOutputSlot, RenderMode, RenderTarget, RenderTargetDefinition, RenderTargetDefinitionMap, TextureData, TextureDefinition, TextureDefinitionMap, TextureUpdateMode, TextureMap, TextureSource, TextureValue, TypedUniform, UniformMat4Value, UniformMap, UniformType, UniformValue } from '../core/types.js';
|
|
11
|
+
export type { LoadedTexture, TextureDecodeOptions, TextureLoadOptions } from '../core/texture-loader.js';
|
|
12
|
+
export type { FragMaterial, FragMaterialInput, MaterialIncludes, MaterialDefineValue, MaterialDefines, TypedMaterialDefineValue } from '../core/material.js';
|
|
13
|
+
export type { MotionGPUContext } from './motiongpu-context.js';
|
|
14
|
+
export type { UseFrameOptions, UseFrameResult } from './frame-context.js';
|
|
15
|
+
export type { TextureUrlInput, UseTextureResult } from './use-texture.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React adapter entrypoint for MotionGPU.
|
|
3
|
+
*/
|
|
4
|
+
export { FragCanvas } from './FragCanvas.js';
|
|
5
|
+
export { defineMaterial } from '../core/material.js';
|
|
6
|
+
export { BlitPass, CopyPass, ShaderPass } from '../passes/index.js';
|
|
7
|
+
export { useMotionGPU } from './motiongpu-context.js';
|
|
8
|
+
export { useFrame } from './frame-context.js';
|
|
9
|
+
export { useTexture } from './use-texture.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { RenderMode } from '../core/types.js';
|
|
2
|
+
import type { CurrentReadable, CurrentWritable } from '../core/current-value.js';
|
|
3
|
+
import type { FrameProfilingSnapshot, FrameRunTimings, FrameScheduleSnapshot } from '../core/frame-registry.js';
|
|
4
|
+
import type { MotionGPUScheduler as CoreMotionGPUScheduler } from '../core/scheduler-helpers.js';
|
|
5
|
+
/**
|
|
6
|
+
* React context payload exposed by `<FragCanvas>`.
|
|
7
|
+
*/
|
|
8
|
+
export interface MotionGPUContext {
|
|
9
|
+
/**
|
|
10
|
+
* Underlying canvas element used by the renderer.
|
|
11
|
+
*/
|
|
12
|
+
canvas: HTMLCanvasElement | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Reactive canvas pixel size.
|
|
15
|
+
*/
|
|
16
|
+
size: CurrentReadable<{
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Device pixel ratio multiplier.
|
|
22
|
+
*/
|
|
23
|
+
dpr: CurrentWritable<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Max frame delta clamp passed to scheduled callbacks.
|
|
26
|
+
*/
|
|
27
|
+
maxDelta: CurrentWritable<number>;
|
|
28
|
+
/**
|
|
29
|
+
* Scheduler render mode (`always`, `on-demand`, `manual`).
|
|
30
|
+
*/
|
|
31
|
+
renderMode: CurrentWritable<RenderMode>;
|
|
32
|
+
/**
|
|
33
|
+
* Global toggle for automatic rendering.
|
|
34
|
+
*/
|
|
35
|
+
autoRender: CurrentWritable<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Namespaced user context store shared within the canvas subtree.
|
|
38
|
+
*/
|
|
39
|
+
user: MotionGPUUserContext;
|
|
40
|
+
/**
|
|
41
|
+
* Marks current frame as invalidated.
|
|
42
|
+
*/
|
|
43
|
+
invalidate: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Requests one manual frame advance.
|
|
46
|
+
*/
|
|
47
|
+
advance: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Public scheduler API.
|
|
50
|
+
*/
|
|
51
|
+
scheduler: MotionGPUScheduler;
|
|
52
|
+
}
|
|
53
|
+
export type MotionGPUScheduler = CoreMotionGPUScheduler;
|
|
54
|
+
export type { FrameProfilingSnapshot, FrameRunTimings, FrameScheduleSnapshot };
|
|
55
|
+
/**
|
|
56
|
+
* Namespace identifier for user-owned context entries.
|
|
57
|
+
*/
|
|
58
|
+
export type MotionGPUUserNamespace = string | symbol;
|
|
59
|
+
/**
|
|
60
|
+
* Shared user context store exposed by `FragCanvas`.
|
|
61
|
+
*/
|
|
62
|
+
export type MotionGPUUserContext = CurrentWritable<Record<MotionGPUUserNamespace, unknown>>;
|
|
63
|
+
/**
|
|
64
|
+
* Internal React context container.
|
|
65
|
+
*/
|
|
66
|
+
export declare const MotionGPUReactContext: import("react").Context<MotionGPUContext | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Returns active MotionGPU runtime context.
|
|
69
|
+
*
|
|
70
|
+
* @returns Active context.
|
|
71
|
+
* @throws {Error} When called outside `<FragCanvas>`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function useMotionGPU(): MotionGPUContext;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Internal React context container.
|
|
4
|
+
*/
|
|
5
|
+
export const MotionGPUReactContext = createContext(null);
|
|
6
|
+
/**
|
|
7
|
+
* Returns active MotionGPU runtime context.
|
|
8
|
+
*
|
|
9
|
+
* @returns Active context.
|
|
10
|
+
* @throws {Error} When called outside `<FragCanvas>`.
|
|
11
|
+
*/
|
|
12
|
+
export function useMotionGPU() {
|
|
13
|
+
const context = useContext(MotionGPUReactContext);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error('useMotionGPU must be used inside <FragCanvas>');
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CurrentReadable } from '../core/current-value.js';
|
|
2
|
+
import { type MotionGPUUserNamespace } from './motiongpu-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Internal shape of the user context store.
|
|
5
|
+
*/
|
|
6
|
+
type UserContextStore = Record<MotionGPUUserNamespace, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Controls how a namespaced user context value behaves when already present.
|
|
9
|
+
*/
|
|
10
|
+
export interface SetMotionGPUUserContextOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Conflict strategy when namespace already exists:
|
|
13
|
+
* - `skip`: keep current value
|
|
14
|
+
* - `replace`: replace current value
|
|
15
|
+
* - `merge`: shallow merge object values, fallback to replace otherwise
|
|
16
|
+
*
|
|
17
|
+
* @default 'skip'
|
|
18
|
+
*/
|
|
19
|
+
existing?: 'merge' | 'replace' | 'skip';
|
|
20
|
+
/**
|
|
21
|
+
* How function inputs should be interpreted:
|
|
22
|
+
* - `factory`: call function and store its return value
|
|
23
|
+
* - `value`: store function itself
|
|
24
|
+
*
|
|
25
|
+
* @default 'factory'
|
|
26
|
+
*/
|
|
27
|
+
functionValue?: 'factory' | 'value';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns a read-only view of the entire motiongpu user context store.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useMotionGPUUserContext<UC extends UserContextStore = UserContextStore>(): CurrentReadable<UC>;
|
|
33
|
+
/**
|
|
34
|
+
* Reads a namespaced user context value as a reactive readable store.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useMotionGPUUserContext<UC extends UserContextStore = UserContextStore, K extends keyof UC & MotionGPUUserNamespace = keyof UC & MotionGPUUserNamespace>(namespace: K): CurrentReadable<UC[K] | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Returns a stable setter bound to the active MotionGPU user context store.
|
|
39
|
+
*
|
|
40
|
+
* @returns Setter function that preserves namespace write semantics.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useSetMotionGPUUserContext(): <UCT = unknown>(namespace: MotionGPUUserNamespace, value: UCT | (() => UCT), options?: SetMotionGPUUserContextOptions) => UCT | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Sets a namespaced user context value with explicit write semantics.
|
|
45
|
+
*
|
|
46
|
+
* Returns the effective value stored under the namespace.
|
|
47
|
+
*/
|
|
48
|
+
export declare function setMotionGPUUserContext<UCT = unknown>(namespace: MotionGPUUserNamespace, value: UCT | (() => UCT), options?: SetMotionGPUUserContextOptions): UCT | undefined;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useMotionGPU } from './motiongpu-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a value is a non-array object suitable for shallow merge.
|
|
5
|
+
*/
|
|
6
|
+
function isObjectEntry(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Sets a namespaced user context value in the provided user store.
|
|
11
|
+
*
|
|
12
|
+
* Returns the effective value stored under the namespace.
|
|
13
|
+
*/
|
|
14
|
+
function setMotionGPUUserContextInStore(userStore, namespace, value, options) {
|
|
15
|
+
const mode = options?.existing ?? 'skip';
|
|
16
|
+
const functionValueMode = options?.functionValue ?? 'factory';
|
|
17
|
+
let resolvedValue;
|
|
18
|
+
userStore.update((context) => {
|
|
19
|
+
const hasExisting = namespace in context;
|
|
20
|
+
if (hasExisting && mode === 'skip') {
|
|
21
|
+
resolvedValue = context[namespace];
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
const nextValue = typeof value === 'function' && functionValueMode === 'factory'
|
|
25
|
+
? value()
|
|
26
|
+
: value;
|
|
27
|
+
if (hasExisting && mode === 'merge') {
|
|
28
|
+
const currentValue = context[namespace];
|
|
29
|
+
if (isObjectEntry(currentValue) && isObjectEntry(nextValue)) {
|
|
30
|
+
resolvedValue = {
|
|
31
|
+
...currentValue,
|
|
32
|
+
...nextValue
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
...context,
|
|
36
|
+
[namespace]: resolvedValue
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
resolvedValue = nextValue;
|
|
41
|
+
return {
|
|
42
|
+
...context,
|
|
43
|
+
[namespace]: nextValue
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
return resolvedValue;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read-only user context hook:
|
|
50
|
+
* - no args: returns full user context store
|
|
51
|
+
* - namespace: returns namespaced store view
|
|
52
|
+
*
|
|
53
|
+
* @param namespace - Optional namespace key.
|
|
54
|
+
*/
|
|
55
|
+
export function useMotionGPUUserContext(namespace) {
|
|
56
|
+
const userStore = useMotionGPU().user;
|
|
57
|
+
const allStore = useMemo(() => ({
|
|
58
|
+
get current() {
|
|
59
|
+
return userStore.current;
|
|
60
|
+
},
|
|
61
|
+
subscribe(run) {
|
|
62
|
+
return userStore.subscribe((context) => run(context));
|
|
63
|
+
}
|
|
64
|
+
}), [userStore]);
|
|
65
|
+
const scopedStore = useMemo(() => ({
|
|
66
|
+
get current() {
|
|
67
|
+
return userStore.current[namespace];
|
|
68
|
+
},
|
|
69
|
+
subscribe(run) {
|
|
70
|
+
return userStore.subscribe((context) => run(context[namespace]));
|
|
71
|
+
}
|
|
72
|
+
}), [namespace, userStore]);
|
|
73
|
+
if (namespace === undefined) {
|
|
74
|
+
return allStore;
|
|
75
|
+
}
|
|
76
|
+
return scopedStore;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns a stable setter bound to the active MotionGPU user context store.
|
|
80
|
+
*
|
|
81
|
+
* @returns Setter function that preserves namespace write semantics.
|
|
82
|
+
*/
|
|
83
|
+
export function useSetMotionGPUUserContext() {
|
|
84
|
+
const userStore = useMotionGPU().user;
|
|
85
|
+
return useCallback((namespace, value, options) => setMotionGPUUserContextInStore(userStore, namespace, value, options), [userStore]);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Sets a namespaced user context value with explicit write semantics.
|
|
89
|
+
*
|
|
90
|
+
* Returns the effective value stored under the namespace.
|
|
91
|
+
*/
|
|
92
|
+
export function setMotionGPUUserContext(namespace, value, options) {
|
|
93
|
+
return setMotionGPUUserContextInStore(useMotionGPU().user, namespace, value, options);
|
|
94
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type CurrentReadable } from '../core/current-value.js';
|
|
2
|
+
import { type LoadedTexture, type TextureLoadOptions } from '../core/texture-loader.js';
|
|
3
|
+
import { type MotionGPUErrorReport } from '../core/error-report.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reactive state returned by `useTexture`.
|
|
6
|
+
*/
|
|
7
|
+
export interface UseTextureResult {
|
|
8
|
+
/**
|
|
9
|
+
* Loaded textures or `null` when unavailable/failed.
|
|
10
|
+
*/
|
|
11
|
+
textures: CurrentReadable<LoadedTexture[] | null>;
|
|
12
|
+
/**
|
|
13
|
+
* `true` while an active load request is running.
|
|
14
|
+
*/
|
|
15
|
+
loading: CurrentReadable<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Last loading error.
|
|
18
|
+
*/
|
|
19
|
+
error: CurrentReadable<Error | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Last loading error normalized to MotionGPU diagnostics report shape.
|
|
22
|
+
*/
|
|
23
|
+
errorReport: CurrentReadable<MotionGPUErrorReport | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Reloads all textures using current URL input.
|
|
26
|
+
*/
|
|
27
|
+
reload: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Supported URL input variants for `useTexture`.
|
|
31
|
+
*/
|
|
32
|
+
export type TextureUrlInput = string[] | (() => string[]);
|
|
33
|
+
/**
|
|
34
|
+
* Loads textures from URLs and exposes reactive loading/error state.
|
|
35
|
+
*
|
|
36
|
+
* @param urlInput - URLs array or lazy URL provider.
|
|
37
|
+
* @param options - Loader options passed to URL fetch/decode pipeline.
|
|
38
|
+
* @returns Reactive texture loading state with reload support.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useTexture(urlInput: TextureUrlInput, options?: TextureLoadOptions): UseTextureResult;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { createCurrentWritable as currentWritable } from '../core/current-value.js';
|
|
3
|
+
import { isAbortError, loadTexturesFromUrls } from '../core/texture-loader.js';
|
|
4
|
+
import { toMotionGPUErrorReport } from '../core/error-report.js';
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes unknown thrown values to an `Error` instance.
|
|
7
|
+
*/
|
|
8
|
+
function toError(error) {
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
return new Error('Unknown texture loading error');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Releases GPU-side resources for a list of loaded textures.
|
|
16
|
+
*/
|
|
17
|
+
function disposeTextures(list) {
|
|
18
|
+
for (const texture of list ?? []) {
|
|
19
|
+
texture.dispose();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function mergeAbortSignals(primary, secondary) {
|
|
23
|
+
if (!secondary) {
|
|
24
|
+
return {
|
|
25
|
+
signal: primary,
|
|
26
|
+
dispose: () => { }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (typeof AbortSignal.any === 'function') {
|
|
30
|
+
return {
|
|
31
|
+
signal: AbortSignal.any([primary, secondary]),
|
|
32
|
+
dispose: () => { }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const fallback = new AbortController();
|
|
36
|
+
let disposed = false;
|
|
37
|
+
const cleanup = () => {
|
|
38
|
+
if (disposed) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
disposed = true;
|
|
42
|
+
primary.removeEventListener('abort', abort);
|
|
43
|
+
secondary.removeEventListener('abort', abort);
|
|
44
|
+
};
|
|
45
|
+
const abort = () => fallback.abort();
|
|
46
|
+
primary.addEventListener('abort', abort, { once: true });
|
|
47
|
+
secondary.addEventListener('abort', abort, { once: true });
|
|
48
|
+
return {
|
|
49
|
+
signal: fallback.signal,
|
|
50
|
+
dispose: cleanup
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Loads textures from URLs and exposes reactive loading/error state.
|
|
55
|
+
*
|
|
56
|
+
* @param urlInput - URLs array or lazy URL provider.
|
|
57
|
+
* @param options - Loader options passed to URL fetch/decode pipeline.
|
|
58
|
+
* @returns Reactive texture loading state with reload support.
|
|
59
|
+
*/
|
|
60
|
+
export function useTexture(urlInput, options = {}) {
|
|
61
|
+
const texturesRef = useRef(currentWritable(null));
|
|
62
|
+
const loadingRef = useRef(currentWritable(true));
|
|
63
|
+
const errorRef = useRef(currentWritable(null));
|
|
64
|
+
const errorReportRef = useRef(currentWritable(null));
|
|
65
|
+
const activeControllerRef = useRef(null);
|
|
66
|
+
const runningLoadRef = useRef(null);
|
|
67
|
+
const reloadQueuedRef = useRef(false);
|
|
68
|
+
const requestVersionRef = useRef(0);
|
|
69
|
+
const disposedRef = useRef(false);
|
|
70
|
+
const optionsRef = useRef(options);
|
|
71
|
+
const urlInputRef = useRef(urlInput);
|
|
72
|
+
optionsRef.current = options;
|
|
73
|
+
urlInputRef.current = urlInput;
|
|
74
|
+
const getUrls = useCallback(() => {
|
|
75
|
+
const currentInput = urlInputRef.current;
|
|
76
|
+
return typeof currentInput === 'function' ? currentInput() : currentInput;
|
|
77
|
+
}, []);
|
|
78
|
+
const executeLoad = useCallback(async () => {
|
|
79
|
+
if (disposedRef.current) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const version = ++requestVersionRef.current;
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
activeControllerRef.current = controller;
|
|
85
|
+
loadingRef.current.set(true);
|
|
86
|
+
errorRef.current.set(null);
|
|
87
|
+
errorReportRef.current.set(null);
|
|
88
|
+
const previous = texturesRef.current.current;
|
|
89
|
+
const mergedSignal = mergeAbortSignals(controller.signal, optionsRef.current.signal);
|
|
90
|
+
try {
|
|
91
|
+
const loaded = await loadTexturesFromUrls(getUrls(), {
|
|
92
|
+
...optionsRef.current,
|
|
93
|
+
signal: mergedSignal.signal
|
|
94
|
+
});
|
|
95
|
+
if (disposedRef.current || version !== requestVersionRef.current) {
|
|
96
|
+
disposeTextures(loaded);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
texturesRef.current.set(loaded);
|
|
100
|
+
disposeTextures(previous);
|
|
101
|
+
}
|
|
102
|
+
catch (nextError) {
|
|
103
|
+
if (disposedRef.current || version !== requestVersionRef.current) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (isAbortError(nextError)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
disposeTextures(previous);
|
|
110
|
+
texturesRef.current.set(null);
|
|
111
|
+
const normalizedError = toError(nextError);
|
|
112
|
+
errorRef.current.set(normalizedError);
|
|
113
|
+
errorReportRef.current.set(toMotionGPUErrorReport(normalizedError, 'initialization'));
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (!disposedRef.current && version === requestVersionRef.current) {
|
|
117
|
+
loadingRef.current.set(false);
|
|
118
|
+
}
|
|
119
|
+
if (activeControllerRef.current === controller) {
|
|
120
|
+
activeControllerRef.current = null;
|
|
121
|
+
}
|
|
122
|
+
mergedSignal.dispose();
|
|
123
|
+
}
|
|
124
|
+
}, [getUrls]);
|
|
125
|
+
const runLoadLoop = useCallback(async () => {
|
|
126
|
+
do {
|
|
127
|
+
reloadQueuedRef.current = false;
|
|
128
|
+
await executeLoad();
|
|
129
|
+
} while (reloadQueuedRef.current && !disposedRef.current);
|
|
130
|
+
}, [executeLoad]);
|
|
131
|
+
const load = useCallback(() => {
|
|
132
|
+
activeControllerRef.current?.abort();
|
|
133
|
+
if (runningLoadRef.current) {
|
|
134
|
+
reloadQueuedRef.current = true;
|
|
135
|
+
return runningLoadRef.current;
|
|
136
|
+
}
|
|
137
|
+
const pending = runLoadLoop();
|
|
138
|
+
const trackedPending = pending.finally(() => {
|
|
139
|
+
if (runningLoadRef.current === trackedPending) {
|
|
140
|
+
runningLoadRef.current = null;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
runningLoadRef.current = trackedPending;
|
|
144
|
+
return trackedPending;
|
|
145
|
+
}, [runLoadLoop]);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
void load();
|
|
148
|
+
return () => {
|
|
149
|
+
disposedRef.current = true;
|
|
150
|
+
requestVersionRef.current += 1;
|
|
151
|
+
activeControllerRef.current?.abort();
|
|
152
|
+
disposeTextures(texturesRef.current.current);
|
|
153
|
+
};
|
|
154
|
+
}, [load]);
|
|
155
|
+
return {
|
|
156
|
+
textures: texturesRef.current,
|
|
157
|
+
loading: loadingRef.current,
|
|
158
|
+
error: errorRef.current,
|
|
159
|
+
errorReport: errorReportRef.current,
|
|
160
|
+
reload: load
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -66,20 +66,18 @@
|
|
|
66
66
|
let errorReport = $state<MotionGPUErrorReport | null>(null);
|
|
67
67
|
let errorHistory = $state<MotionGPUErrorReport[]>([]);
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
let normalizedErrorHistoryLimit = $derived.by(() => {
|
|
70
70
|
if (!Number.isFinite(errorHistoryLimit) || errorHistoryLimit <= 0) {
|
|
71
71
|
return 0;
|
|
72
72
|
}
|
|
73
73
|
return Math.floor(errorHistoryLimit);
|
|
74
|
-
};
|
|
74
|
+
});
|
|
75
75
|
|
|
76
76
|
const bindCanvas = (node: HTMLCanvasElement) => {
|
|
77
77
|
canvas = node;
|
|
78
|
-
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
canvas = undefined;
|
|
82
|
-
}
|
|
78
|
+
return () => {
|
|
79
|
+
if (canvas === node) {
|
|
80
|
+
canvas = undefined;
|
|
83
81
|
}
|
|
84
82
|
};
|
|
85
83
|
};
|
|
@@ -144,26 +142,14 @@
|
|
|
144
142
|
|
|
145
143
|
$effect(() => {
|
|
146
144
|
renderModeState.set(renderMode);
|
|
147
|
-
requestFrame();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
$effect(() => {
|
|
151
145
|
autoRenderState.set(autoRender);
|
|
152
|
-
requestFrame();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
$effect(() => {
|
|
156
146
|
maxDeltaState.set(maxDelta);
|
|
157
|
-
requestFrame();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
$effect(() => {
|
|
161
147
|
dprState.set(dpr);
|
|
162
148
|
requestFrame();
|
|
163
149
|
});
|
|
164
150
|
|
|
165
151
|
$effect(() => {
|
|
166
|
-
const limit =
|
|
152
|
+
const limit = normalizedErrorHistoryLimit;
|
|
167
153
|
if (limit <= 0) {
|
|
168
154
|
if (errorHistory.length === 0) {
|
|
169
155
|
return;
|
|
@@ -189,7 +175,7 @@
|
|
|
189
175
|
'initialization'
|
|
190
176
|
);
|
|
191
177
|
errorReport = report;
|
|
192
|
-
const historyLimit =
|
|
178
|
+
const historyLimit = normalizedErrorHistoryLimit;
|
|
193
179
|
if (historyLimit > 0) {
|
|
194
180
|
const nextHistory = [report].slice(-historyLimit);
|
|
195
181
|
errorHistory = nextHistory;
|
|
@@ -232,7 +218,7 @@
|
|
|
232
218
|
</script>
|
|
233
219
|
|
|
234
220
|
<div class="motiongpu-canvas-wrap">
|
|
235
|
-
<canvas
|
|
221
|
+
<canvas {@attach bindCanvas} class={className} {style}></canvas>
|
|
236
222
|
{#if showErrorOverlay && errorReport}
|
|
237
223
|
{#if errorRenderer}
|
|
238
224
|
{@render errorRenderer(errorReport)}
|
|
@@ -14,33 +14,18 @@
|
|
|
14
14
|
: (input ?? document.body);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const portal = (node: HTMLDivElement
|
|
18
|
-
|
|
17
|
+
const portal = (node: HTMLDivElement) => {
|
|
18
|
+
const targetElement = resolveTargetElement(target);
|
|
19
19
|
targetElement.appendChild(node);
|
|
20
20
|
|
|
21
|
-
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (nextTargetElement === targetElement) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (node.parentNode === targetElement) {
|
|
29
|
-
targetElement.removeChild(node);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
nextTargetElement.appendChild(node);
|
|
33
|
-
targetElement = nextTargetElement;
|
|
34
|
-
},
|
|
35
|
-
destroy() {
|
|
36
|
-
if (node.parentNode === targetElement) {
|
|
37
|
-
targetElement.removeChild(node);
|
|
38
|
-
}
|
|
21
|
+
return () => {
|
|
22
|
+
if (node.parentNode === targetElement) {
|
|
23
|
+
targetElement.removeChild(node);
|
|
39
24
|
}
|
|
40
25
|
};
|
|
41
26
|
};
|
|
42
27
|
</script>
|
|
43
28
|
|
|
44
|
-
<div
|
|
29
|
+
<div {@attach portal}>
|
|
45
30
|
{@render children?.()}
|
|
46
31
|
</div>
|