@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.
@@ -0,0 +1,345 @@
1
+ import { createCurrentWritable as currentWritable } from '../core/current-value.js';
2
+ import { toMotionGPUErrorReport, type MotionGPUErrorReport } from '../core/error-report.js';
3
+ import type { FragMaterial } from '../core/material.js';
4
+ import { createFrameRegistry } from '../core/frame-registry.js';
5
+ import { createMotionGPURuntimeLoop } from '../core/runtime-loop.js';
6
+ import type {
7
+ OutputColorSpace,
8
+ RenderPass,
9
+ RenderMode,
10
+ RenderTargetDefinitionMap
11
+ } from '../core/types.js';
12
+ import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
13
+ import { FrameRegistryReactContext } from './frame-context.js';
14
+ import { MotionGPUErrorOverlay } from './MotionGPUErrorOverlay.js';
15
+ import { MotionGPUReactContext, type MotionGPUContext } from './motiongpu-context.js';
16
+
17
+ export interface FragCanvasProps {
18
+ material: FragMaterial;
19
+ renderTargets?: RenderTargetDefinitionMap;
20
+ passes?: RenderPass[];
21
+ clearColor?: [number, number, number, number];
22
+ outputColorSpace?: OutputColorSpace;
23
+ renderMode?: RenderMode;
24
+ autoRender?: boolean;
25
+ maxDelta?: number;
26
+ adapterOptions?: GPURequestAdapterOptions;
27
+ deviceDescriptor?: GPUDeviceDescriptor;
28
+ dpr?: number;
29
+ showErrorOverlay?: boolean;
30
+ errorRenderer?: (report: MotionGPUErrorReport) => ReactNode;
31
+ onError?: (report: MotionGPUErrorReport) => void;
32
+ errorHistoryLimit?: number;
33
+ onErrorHistory?: (history: MotionGPUErrorReport[]) => void;
34
+ className?: string;
35
+ style?: CSSProperties;
36
+ children?: ReactNode;
37
+ }
38
+
39
+ interface RuntimePropsSnapshot {
40
+ material: FragMaterial;
41
+ renderTargets: RenderTargetDefinitionMap;
42
+ passes: RenderPass[];
43
+ clearColor: [number, number, number, number];
44
+ outputColorSpace: OutputColorSpace;
45
+ adapterOptions: GPURequestAdapterOptions | undefined;
46
+ deviceDescriptor: GPUDeviceDescriptor | undefined;
47
+ onError: ((report: MotionGPUErrorReport) => void) | undefined;
48
+ errorHistoryLimit: number;
49
+ onErrorHistory: ((history: MotionGPUErrorReport[]) => void) | undefined;
50
+ }
51
+
52
+ interface FragCanvasRuntimeState {
53
+ registry: ReturnType<typeof createFrameRegistry>;
54
+ context: MotionGPUContext;
55
+ canvasRef: { current: HTMLCanvasElement | undefined };
56
+ size: ReturnType<typeof currentWritable<{ width: number; height: number }>>;
57
+ dprState: ReturnType<typeof currentWritable<number>>;
58
+ maxDeltaState: ReturnType<typeof currentWritable<number>>;
59
+ renderModeState: ReturnType<typeof currentWritable<RenderMode>>;
60
+ autoRenderState: ReturnType<typeof currentWritable<boolean>>;
61
+ requestFrameSignalRef: { current: (() => void) | null };
62
+ requestFrame: () => void;
63
+ invalidateFrame: () => void;
64
+ advanceFrame: () => void;
65
+ }
66
+
67
+ function getInitialDpr(): number {
68
+ if (typeof window === 'undefined') {
69
+ return 1;
70
+ }
71
+
72
+ return window.devicePixelRatio ?? 1;
73
+ }
74
+
75
+ function createRuntimeState(initialDpr: number): FragCanvasRuntimeState {
76
+ const registry = createFrameRegistry({ maxDelta: 0.1 });
77
+ const canvasRef = { current: undefined as HTMLCanvasElement | undefined };
78
+ const requestFrameSignalRef = { current: null as (() => void) | null };
79
+ const requestFrame = (): void => {
80
+ requestFrameSignalRef.current?.();
81
+ };
82
+ const invalidateFrame = (): void => {
83
+ registry.invalidate();
84
+ requestFrame();
85
+ };
86
+ const advanceFrame = (): void => {
87
+ registry.advance();
88
+ requestFrame();
89
+ };
90
+
91
+ const size = currentWritable({ width: 0, height: 0 });
92
+ const dprState = currentWritable(initialDpr, requestFrame);
93
+ const maxDeltaState = currentWritable(0.1, (value) => {
94
+ registry.setMaxDelta(value);
95
+ requestFrame();
96
+ });
97
+ const renderModeState = currentWritable<RenderMode>('always', (value) => {
98
+ registry.setRenderMode(value);
99
+ requestFrame();
100
+ });
101
+ const autoRenderState = currentWritable<boolean>(true, (value) => {
102
+ registry.setAutoRender(value);
103
+ requestFrame();
104
+ });
105
+ const userState = currentWritable<Record<string | symbol, unknown>>({});
106
+
107
+ const context: MotionGPUContext = {
108
+ get canvas() {
109
+ return canvasRef.current;
110
+ },
111
+ size,
112
+ dpr: dprState,
113
+ maxDelta: maxDeltaState,
114
+ renderMode: renderModeState,
115
+ autoRender: autoRenderState,
116
+ user: userState,
117
+ invalidate: invalidateFrame,
118
+ advance: advanceFrame,
119
+ scheduler: {
120
+ createStage: registry.createStage,
121
+ getStage: registry.getStage,
122
+ setDiagnosticsEnabled: registry.setDiagnosticsEnabled,
123
+ getDiagnosticsEnabled: registry.getDiagnosticsEnabled,
124
+ getLastRunTimings: registry.getLastRunTimings,
125
+ getSchedule: registry.getSchedule,
126
+ setProfilingEnabled: registry.setProfilingEnabled,
127
+ setProfilingWindow: registry.setProfilingWindow,
128
+ resetProfiling: registry.resetProfiling,
129
+ getProfilingEnabled: registry.getProfilingEnabled,
130
+ getProfilingWindow: registry.getProfilingWindow,
131
+ getProfilingSnapshot: registry.getProfilingSnapshot
132
+ }
133
+ };
134
+
135
+ return {
136
+ registry,
137
+ context,
138
+ canvasRef,
139
+ size,
140
+ dprState,
141
+ maxDeltaState,
142
+ renderModeState,
143
+ autoRenderState,
144
+ requestFrameSignalRef,
145
+ requestFrame,
146
+ invalidateFrame,
147
+ advanceFrame
148
+ };
149
+ }
150
+
151
+ function getNormalizedErrorHistoryLimit(value: number): number {
152
+ if (!Number.isFinite(value) || value <= 0) {
153
+ return 0;
154
+ }
155
+
156
+ return Math.floor(value);
157
+ }
158
+
159
+ export function FragCanvas({
160
+ material,
161
+ renderTargets = {},
162
+ passes = [],
163
+ clearColor = [0, 0, 0, 1],
164
+ outputColorSpace = 'srgb',
165
+ renderMode = 'always',
166
+ autoRender = true,
167
+ maxDelta = 0.1,
168
+ adapterOptions = undefined,
169
+ deviceDescriptor = undefined,
170
+ dpr = getInitialDpr(),
171
+ showErrorOverlay = true,
172
+ errorRenderer,
173
+ onError = undefined,
174
+ errorHistoryLimit = 0,
175
+ onErrorHistory = undefined,
176
+ className = '',
177
+ style,
178
+ children
179
+ }: FragCanvasProps) {
180
+ const runtimeRef = useRef<FragCanvasRuntimeState | null>(null);
181
+ if (!runtimeRef.current) {
182
+ runtimeRef.current = createRuntimeState(getInitialDpr());
183
+ }
184
+ const runtime = runtimeRef.current;
185
+
186
+ const runtimePropsRef = useRef<RuntimePropsSnapshot>({
187
+ material,
188
+ renderTargets,
189
+ passes,
190
+ clearColor,
191
+ outputColorSpace,
192
+ adapterOptions,
193
+ deviceDescriptor,
194
+ onError,
195
+ errorHistoryLimit,
196
+ onErrorHistory
197
+ });
198
+ runtimePropsRef.current = {
199
+ material,
200
+ renderTargets,
201
+ passes,
202
+ clearColor,
203
+ outputColorSpace,
204
+ adapterOptions,
205
+ deviceDescriptor,
206
+ onError,
207
+ errorHistoryLimit,
208
+ onErrorHistory
209
+ };
210
+
211
+ const [errorReport, setErrorReport] = useState<MotionGPUErrorReport | null>(null);
212
+ const [errorHistory, setErrorHistory] = useState<MotionGPUErrorReport[]>([]);
213
+
214
+ useEffect(() => {
215
+ runtime.renderModeState.set(renderMode);
216
+ }, [renderMode, runtime]);
217
+
218
+ useEffect(() => {
219
+ runtime.autoRenderState.set(autoRender);
220
+ }, [autoRender, runtime]);
221
+
222
+ useEffect(() => {
223
+ runtime.maxDeltaState.set(maxDelta);
224
+ }, [maxDelta, runtime]);
225
+
226
+ useEffect(() => {
227
+ runtime.dprState.set(dpr);
228
+ }, [dpr, runtime]);
229
+
230
+ useEffect(() => {
231
+ const limit = getNormalizedErrorHistoryLimit(errorHistoryLimit);
232
+ if (limit <= 0) {
233
+ if (errorHistory.length === 0) {
234
+ return;
235
+ }
236
+ setErrorHistory([]);
237
+ onErrorHistory?.([]);
238
+ return;
239
+ }
240
+
241
+ if (errorHistory.length <= limit) {
242
+ return;
243
+ }
244
+
245
+ const trimmed = errorHistory.slice(errorHistory.length - limit);
246
+ setErrorHistory(trimmed);
247
+ onErrorHistory?.(trimmed);
248
+ }, [errorHistory, errorHistoryLimit, onErrorHistory]);
249
+
250
+ useEffect(() => {
251
+ const canvas = runtime.canvasRef.current;
252
+ if (!canvas) {
253
+ const report = toMotionGPUErrorReport(
254
+ new Error('Canvas element is not available'),
255
+ 'initialization'
256
+ );
257
+ setErrorReport(report);
258
+ const historyLimit = getNormalizedErrorHistoryLimit(
259
+ runtimePropsRef.current.errorHistoryLimit
260
+ );
261
+ if (historyLimit > 0) {
262
+ const nextHistory = [report].slice(-historyLimit);
263
+ setErrorHistory(nextHistory);
264
+ runtimePropsRef.current.onErrorHistory?.(nextHistory);
265
+ }
266
+ runtimePropsRef.current.onError?.(report);
267
+ return () => {
268
+ runtime.registry.clear();
269
+ };
270
+ }
271
+
272
+ const runtimeLoop = createMotionGPURuntimeLoop({
273
+ canvas,
274
+ registry: runtime.registry,
275
+ size: runtime.size,
276
+ dpr: runtime.dprState,
277
+ maxDelta: runtime.maxDeltaState,
278
+ getMaterial: () => runtimePropsRef.current.material,
279
+ getRenderTargets: () => runtimePropsRef.current.renderTargets,
280
+ getPasses: () => runtimePropsRef.current.passes,
281
+ getClearColor: () => runtimePropsRef.current.clearColor,
282
+ getOutputColorSpace: () => runtimePropsRef.current.outputColorSpace,
283
+ getAdapterOptions: () => runtimePropsRef.current.adapterOptions,
284
+ getDeviceDescriptor: () => runtimePropsRef.current.deviceDescriptor,
285
+ getOnError: () => runtimePropsRef.current.onError,
286
+ getErrorHistoryLimit: () => runtimePropsRef.current.errorHistoryLimit,
287
+ getOnErrorHistory: () => runtimePropsRef.current.onErrorHistory,
288
+ reportErrorHistory: (history) => {
289
+ setErrorHistory(history);
290
+ },
291
+ reportError: (report) => {
292
+ setErrorReport(report);
293
+ }
294
+ });
295
+ runtime.requestFrameSignalRef.current = runtimeLoop.requestFrame;
296
+
297
+ return () => {
298
+ runtime.requestFrameSignalRef.current = null;
299
+ runtimeLoop.destroy();
300
+ };
301
+ }, [runtime]);
302
+
303
+ const canvasStyle: CSSProperties = {
304
+ position: 'absolute',
305
+ inset: 0,
306
+ display: 'block',
307
+ width: '100%',
308
+ height: '100%',
309
+ ...style
310
+ };
311
+
312
+ return (
313
+ <FrameRegistryReactContext.Provider value={runtime.registry}>
314
+ <MotionGPUReactContext.Provider value={runtime.context}>
315
+ <div
316
+ className="motiongpu-canvas-wrap"
317
+ style={{
318
+ position: 'relative',
319
+ width: '100%',
320
+ height: '100%',
321
+ minWidth: 0,
322
+ minHeight: 0,
323
+ overflow: 'hidden'
324
+ }}
325
+ >
326
+ <canvas
327
+ className={className}
328
+ style={canvasStyle}
329
+ ref={(node) => {
330
+ runtime.canvasRef.current = node ?? undefined;
331
+ }}
332
+ />
333
+ {showErrorOverlay && errorReport ? (
334
+ errorRenderer ? (
335
+ errorRenderer(errorReport)
336
+ ) : (
337
+ <MotionGPUErrorOverlay report={errorReport} />
338
+ )
339
+ ) : null}
340
+ {children}
341
+ </div>
342
+ </MotionGPUReactContext.Provider>
343
+ </FrameRegistryReactContext.Provider>
344
+ );
345
+ }
@@ -0,0 +1,6 @@
1
+ import type { MotionGPUErrorReport } from '../core/error-report.js';
2
+ interface MotionGPUErrorOverlayProps {
3
+ report: MotionGPUErrorReport;
4
+ }
5
+ export declare function MotionGPUErrorOverlay({ report }: MotionGPUErrorOverlayProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Portal } from './Portal.js';
3
+ function normalizeErrorText(value) {
4
+ return value
5
+ .trim()
6
+ .replace(/[.:!]+$/g, '')
7
+ .toLowerCase();
8
+ }
9
+ function shouldShowErrorMessage(value) {
10
+ return normalizeErrorText(value.message) !== normalizeErrorText(value.title);
11
+ }
12
+ export function MotionGPUErrorOverlay({ report }) {
13
+ const detailsSummary = report.source ? 'Additional diagnostics' : 'Technical details';
14
+ return (_jsx(Portal, { children: _jsx("div", { className: "motiongpu-error-overlay", role: "presentation", style: {
15
+ position: 'fixed',
16
+ inset: 0,
17
+ display: 'grid',
18
+ placeItems: 'center',
19
+ padding: '1rem',
20
+ background: 'rgba(12, 12, 14, 0.38)',
21
+ backdropFilter: 'blur(10px)',
22
+ zIndex: 2147483647
23
+ }, children: _jsxs("section", { role: "alertdialog", "aria-live": "assertive", "aria-modal": "true", "data-testid": "motiongpu-error", style: {
24
+ width: 'min(52rem, calc(100vw - 1.5rem))',
25
+ maxHeight: 'min(84vh, 44rem)',
26
+ overflow: 'auto',
27
+ margin: 0,
28
+ padding: '1rem',
29
+ border: '1px solid rgba(107, 107, 107, 0.2)',
30
+ borderRadius: '1rem',
31
+ boxSizing: 'border-box',
32
+ background: '#ffffff',
33
+ color: '#262626',
34
+ fontSize: '0.875rem',
35
+ lineHeight: 1.45
36
+ }, children: [_jsxs("header", { style: { display: 'grid', gap: '0.5rem' }, children: [_jsx("p", { style: {
37
+ margin: 0,
38
+ fontSize: '0.66rem',
39
+ letterSpacing: '0.08em',
40
+ textTransform: 'uppercase',
41
+ color: '#5f6672'
42
+ }, children: report.phase }), _jsx("h2", { style: { margin: 0, fontSize: '1.1rem', lineHeight: 1.2 }, children: report.title })] }), _jsxs("div", { style: { display: 'grid', gap: '0.5rem', marginTop: '0.75rem' }, children: [shouldShowErrorMessage(report) ? _jsx("p", { style: { margin: 0 }, children: report.message }) : null, _jsx("p", { style: { margin: 0, color: '#5f6672' }, children: report.hint })] }), report.source ? (_jsxs("section", { "aria-label": "Source", style: { marginTop: '0.75rem' }, children: [_jsx("h3", { style: { margin: 0, fontSize: '0.8rem', textTransform: 'uppercase' }, children: "Source" }), _jsxs("div", { style: { marginTop: '0.4rem', border: '1px solid rgba(107, 107, 107, 0.2)' }, children: [_jsx("div", { style: {
43
+ padding: '0.45rem 0.6rem',
44
+ borderBottom: '1px solid rgba(107, 107, 107, 0.2)'
45
+ }, children: _jsxs("span", { children: [report.source.location, report.source.column ? `, col ${report.source.column}` : ''] }) }), _jsx("div", { style: { display: 'grid' }, children: report.source.snippet.map((snippetLine) => (_jsxs("div", { style: {
46
+ display: 'grid',
47
+ gridTemplateColumns: '2rem minmax(0, 1fr)',
48
+ gap: '0.42rem',
49
+ padding: '0.2rem 0.5rem',
50
+ background: snippetLine.highlight ? 'rgba(255, 105, 0, 0.08)' : undefined
51
+ }, children: [_jsx("span", { children: snippetLine.number }), _jsx("span", { className: "motiongpu-error-source-code", children: snippetLine.code || ' ' })] }, `snippet-${snippetLine.number}`))) })] })] })) : null, report.details.length > 0 ? (_jsxs("details", { open: true, style: { marginTop: '0.75rem' }, children: [_jsx("summary", { children: detailsSummary }), _jsx("pre", { style: { whiteSpace: 'pre-wrap' }, children: report.details.join('\n') })] })) : null, report.stack.length > 0 ? (_jsxs("details", { style: { marginTop: '0.75rem' }, children: [_jsx("summary", { children: "Stack trace" }), _jsx("pre", { style: { whiteSpace: 'pre-wrap' }, children: report.stack.join('\n') })] })) : null] }) }) }));
52
+ }
@@ -0,0 +1,129 @@
1
+ import type { MotionGPUErrorReport } from '../core/error-report.js';
2
+ import { Portal } from './Portal.js';
3
+
4
+ interface MotionGPUErrorOverlayProps {
5
+ report: MotionGPUErrorReport;
6
+ }
7
+
8
+ function normalizeErrorText(value: string): string {
9
+ return value
10
+ .trim()
11
+ .replace(/[.:!]+$/g, '')
12
+ .toLowerCase();
13
+ }
14
+
15
+ function shouldShowErrorMessage(value: MotionGPUErrorReport): boolean {
16
+ return normalizeErrorText(value.message) !== normalizeErrorText(value.title);
17
+ }
18
+
19
+ export function MotionGPUErrorOverlay({ report }: MotionGPUErrorOverlayProps) {
20
+ const detailsSummary = report.source ? 'Additional diagnostics' : 'Technical details';
21
+
22
+ return (
23
+ <Portal>
24
+ <div
25
+ className="motiongpu-error-overlay"
26
+ role="presentation"
27
+ style={{
28
+ position: 'fixed',
29
+ inset: 0,
30
+ display: 'grid',
31
+ placeItems: 'center',
32
+ padding: '1rem',
33
+ background: 'rgba(12, 12, 14, 0.38)',
34
+ backdropFilter: 'blur(10px)',
35
+ zIndex: 2147483647
36
+ }}
37
+ >
38
+ <section
39
+ role="alertdialog"
40
+ aria-live="assertive"
41
+ aria-modal="true"
42
+ data-testid="motiongpu-error"
43
+ style={{
44
+ width: 'min(52rem, calc(100vw - 1.5rem))',
45
+ maxHeight: 'min(84vh, 44rem)',
46
+ overflow: 'auto',
47
+ margin: 0,
48
+ padding: '1rem',
49
+ border: '1px solid rgba(107, 107, 107, 0.2)',
50
+ borderRadius: '1rem',
51
+ boxSizing: 'border-box',
52
+ background: '#ffffff',
53
+ color: '#262626',
54
+ fontSize: '0.875rem',
55
+ lineHeight: 1.45
56
+ }}
57
+ >
58
+ <header style={{ display: 'grid', gap: '0.5rem' }}>
59
+ <p
60
+ style={{
61
+ margin: 0,
62
+ fontSize: '0.66rem',
63
+ letterSpacing: '0.08em',
64
+ textTransform: 'uppercase',
65
+ color: '#5f6672'
66
+ }}
67
+ >
68
+ {report.phase}
69
+ </p>
70
+ <h2 style={{ margin: 0, fontSize: '1.1rem', lineHeight: 1.2 }}>{report.title}</h2>
71
+ </header>
72
+ <div style={{ display: 'grid', gap: '0.5rem', marginTop: '0.75rem' }}>
73
+ {shouldShowErrorMessage(report) ? <p style={{ margin: 0 }}>{report.message}</p> : null}
74
+ <p style={{ margin: 0, color: '#5f6672' }}>{report.hint}</p>
75
+ </div>
76
+
77
+ {report.source ? (
78
+ <section aria-label="Source" style={{ marginTop: '0.75rem' }}>
79
+ <h3 style={{ margin: 0, fontSize: '0.8rem', textTransform: 'uppercase' }}>Source</h3>
80
+ <div style={{ marginTop: '0.4rem', border: '1px solid rgba(107, 107, 107, 0.2)' }}>
81
+ <div
82
+ style={{
83
+ padding: '0.45rem 0.6rem',
84
+ borderBottom: '1px solid rgba(107, 107, 107, 0.2)'
85
+ }}
86
+ >
87
+ <span>
88
+ {report.source.location}
89
+ {report.source.column ? `, col ${report.source.column}` : ''}
90
+ </span>
91
+ </div>
92
+ <div style={{ display: 'grid' }}>
93
+ {report.source.snippet.map((snippetLine) => (
94
+ <div
95
+ key={`snippet-${snippetLine.number}`}
96
+ style={{
97
+ display: 'grid',
98
+ gridTemplateColumns: '2rem minmax(0, 1fr)',
99
+ gap: '0.42rem',
100
+ padding: '0.2rem 0.5rem',
101
+ background: snippetLine.highlight ? 'rgba(255, 105, 0, 0.08)' : undefined
102
+ }}
103
+ >
104
+ <span>{snippetLine.number}</span>
105
+ <span className="motiongpu-error-source-code">{snippetLine.code || ' '}</span>
106
+ </div>
107
+ ))}
108
+ </div>
109
+ </div>
110
+ </section>
111
+ ) : null}
112
+
113
+ {report.details.length > 0 ? (
114
+ <details open style={{ marginTop: '0.75rem' }}>
115
+ <summary>{detailsSummary}</summary>
116
+ <pre style={{ whiteSpace: 'pre-wrap' }}>{report.details.join('\n')}</pre>
117
+ </details>
118
+ ) : null}
119
+ {report.stack.length > 0 ? (
120
+ <details style={{ marginTop: '0.75rem' }}>
121
+ <summary>Stack trace</summary>
122
+ <pre style={{ whiteSpace: 'pre-wrap' }}>{report.stack.join('\n')}</pre>
123
+ </details>
124
+ ) : null}
125
+ </section>
126
+ </div>
127
+ </Portal>
128
+ );
129
+ }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface PortalProps {
3
+ target?: string | HTMLElement | null;
4
+ children?: ReactNode;
5
+ }
6
+ export declare function Portal({ target, children }: PortalProps): import("react").ReactPortal | null;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ function resolveTargetElement(input) {
5
+ if (typeof document === 'undefined') {
6
+ throw new Error('Portal target resolution requires a browser environment');
7
+ }
8
+ return typeof input === 'string'
9
+ ? (document.querySelector(input) ?? document.body)
10
+ : (input ?? document.body);
11
+ }
12
+ export function Portal({ target = 'body', children }) {
13
+ const [targetElement, setTargetElement] = useState(null);
14
+ useEffect(() => {
15
+ if (typeof document === 'undefined') {
16
+ return;
17
+ }
18
+ setTargetElement(resolveTargetElement(target));
19
+ }, [target]);
20
+ if (!targetElement) {
21
+ return null;
22
+ }
23
+ return createPortal(_jsx("div", { children: children ?? null }), targetElement);
24
+ }
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ export interface PortalProps {
5
+ target?: string | HTMLElement | null;
6
+ children?: ReactNode;
7
+ }
8
+
9
+ function resolveTargetElement(input: string | HTMLElement | null | undefined): HTMLElement {
10
+ if (typeof document === 'undefined') {
11
+ throw new Error('Portal target resolution requires a browser environment');
12
+ }
13
+
14
+ return typeof input === 'string'
15
+ ? (document.querySelector<HTMLElement>(input) ?? document.body)
16
+ : (input ?? document.body);
17
+ }
18
+
19
+ export function Portal({ target = 'body', children }: PortalProps) {
20
+ const [targetElement, setTargetElement] = useState<HTMLElement | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (typeof document === 'undefined') {
24
+ return;
25
+ }
26
+ setTargetElement(resolveTargetElement(target));
27
+ }, [target]);
28
+
29
+ if (!targetElement) {
30
+ return null;
31
+ }
32
+
33
+ return createPortal(<div>{children ?? null}</div>, targetElement);
34
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * React adapter advanced entrypoint for MotionGPU.
3
+ */
4
+ export * from './index.js';
5
+ export { applySchedulerPreset, captureSchedulerDebugSnapshot } from '../core/scheduler-helpers.js';
6
+ export { setMotionGPUUserContext, useMotionGPUUserContext, useSetMotionGPUUserContext } from './use-motiongpu-user-context.js';
7
+ export type { ApplySchedulerPresetOptions, SchedulerDebugSnapshot, SchedulerPreset, SchedulerPresetConfig } from '../core/scheduler-helpers.js';
8
+ export type { MotionGPUUserContext, MotionGPUUserNamespace } from './motiongpu-context.js';
9
+ export type { FrameProfilingSnapshot, FrameKey, FrameTaskInvalidation, FrameTaskInvalidationToken, FrameRunTimings, FrameScheduleSnapshot, FrameStage, FrameStageCallback, FrameTimingStats, FrameTask } from '../core/frame-registry.js';
10
+ export type { SetMotionGPUUserContextOptions } from './use-motiongpu-user-context.js';
11
+ export type { RenderPassContext, RenderTarget, UniformLayout, UniformLayoutEntry } from '../core/types.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * React adapter advanced entrypoint for MotionGPU.
3
+ */
4
+ export * from './index.js';
5
+ export { applySchedulerPreset, captureSchedulerDebugSnapshot } from '../core/scheduler-helpers.js';
6
+ export { setMotionGPUUserContext, useMotionGPUUserContext, useSetMotionGPUUserContext } from './use-motiongpu-user-context.js';
@@ -0,0 +1,14 @@
1
+ import type { FrameCallback, FrameKey, FrameProfilingSnapshot, FrameRegistry, FrameRunTimings, FrameScheduleSnapshot, FrameStage, FrameStageCallback, FrameTask, FrameTaskInvalidation, FrameTaskInvalidationToken, UseFrameOptions, UseFrameResult } from '../core/frame-registry.js';
2
+ /**
3
+ * React context container for the active frame registry.
4
+ */
5
+ export declare const FrameRegistryReactContext: import("react").Context<FrameRegistry | null>;
6
+ export type { FrameCallback, FrameKey, FrameProfilingSnapshot, FrameRegistry, FrameRunTimings, FrameScheduleSnapshot, FrameStage, FrameStageCallback, FrameTask, FrameTaskInvalidation, FrameTaskInvalidationToken, UseFrameOptions, UseFrameResult };
7
+ /**
8
+ * Registers a frame callback using an auto-generated task key.
9
+ */
10
+ export declare function useFrame(callback: FrameCallback, options?: UseFrameOptions): UseFrameResult;
11
+ /**
12
+ * Registers a frame callback with an explicit task key.
13
+ */
14
+ export declare function useFrame(key: FrameKey, callback: FrameCallback, options?: UseFrameOptions): UseFrameResult;