@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,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,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;
|