@motion-core/motion-gpu 0.5.0 → 0.7.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.
Files changed (71) hide show
  1. package/README.md +35 -2
  2. package/dist/core/compute-bindgroup-cache.d.ts +13 -0
  3. package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
  4. package/dist/core/compute-bindgroup-cache.js +45 -0
  5. package/dist/core/compute-bindgroup-cache.js.map +1 -0
  6. package/dist/core/compute-shader.d.ts +48 -0
  7. package/dist/core/compute-shader.d.ts.map +1 -1
  8. package/dist/core/compute-shader.js +34 -1
  9. package/dist/core/compute-shader.js.map +1 -1
  10. package/dist/core/error-diagnostics.d.ts +8 -1
  11. package/dist/core/error-diagnostics.d.ts.map +1 -1
  12. package/dist/core/error-diagnostics.js +7 -3
  13. package/dist/core/error-diagnostics.js.map +1 -1
  14. package/dist/core/error-report.d.ts.map +1 -1
  15. package/dist/core/error-report.js +19 -1
  16. package/dist/core/error-report.js.map +1 -1
  17. package/dist/core/material.d.ts.map +1 -1
  18. package/dist/core/material.js +2 -1
  19. package/dist/core/material.js.map +1 -1
  20. package/dist/core/pointer.d.ts +96 -0
  21. package/dist/core/pointer.d.ts.map +1 -0
  22. package/dist/core/pointer.js +71 -0
  23. package/dist/core/pointer.js.map +1 -0
  24. package/dist/core/renderer.d.ts.map +1 -1
  25. package/dist/core/renderer.js +150 -85
  26. package/dist/core/renderer.js.map +1 -1
  27. package/dist/core/runtime-loop.d.ts.map +1 -1
  28. package/dist/core/runtime-loop.js +26 -14
  29. package/dist/core/runtime-loop.js.map +1 -1
  30. package/dist/core/shader.d.ts +7 -2
  31. package/dist/core/shader.d.ts.map +1 -1
  32. package/dist/core/shader.js +1 -0
  33. package/dist/core/shader.js.map +1 -1
  34. package/dist/core/textures.d.ts +4 -0
  35. package/dist/core/textures.d.ts.map +1 -1
  36. package/dist/core/textures.js +2 -1
  37. package/dist/core/textures.js.map +1 -1
  38. package/dist/core/types.d.ts +1 -1
  39. package/dist/core/types.d.ts.map +1 -1
  40. package/dist/react/advanced.js +2 -1
  41. package/dist/react/index.d.ts +2 -0
  42. package/dist/react/index.d.ts.map +1 -1
  43. package/dist/react/index.js +2 -1
  44. package/dist/react/use-pointer.d.ts +94 -0
  45. package/dist/react/use-pointer.d.ts.map +1 -0
  46. package/dist/react/use-pointer.js +285 -0
  47. package/dist/react/use-pointer.js.map +1 -0
  48. package/dist/svelte/advanced.js +2 -1
  49. package/dist/svelte/index.d.ts +2 -0
  50. package/dist/svelte/index.d.ts.map +1 -1
  51. package/dist/svelte/index.js +2 -1
  52. package/dist/svelte/use-pointer.d.ts +94 -0
  53. package/dist/svelte/use-pointer.d.ts.map +1 -0
  54. package/dist/svelte/use-pointer.js +292 -0
  55. package/dist/svelte/use-pointer.js.map +1 -0
  56. package/package.json +1 -1
  57. package/src/lib/core/compute-bindgroup-cache.ts +73 -0
  58. package/src/lib/core/compute-shader.ts +86 -0
  59. package/src/lib/core/error-diagnostics.ts +29 -4
  60. package/src/lib/core/error-report.ts +26 -1
  61. package/src/lib/core/material.ts +2 -1
  62. package/src/lib/core/pointer.ts +177 -0
  63. package/src/lib/core/renderer.ts +198 -92
  64. package/src/lib/core/runtime-loop.ts +37 -16
  65. package/src/lib/core/shader.ts +13 -2
  66. package/src/lib/core/textures.ts +6 -1
  67. package/src/lib/core/types.ts +1 -1
  68. package/src/lib/react/index.ts +10 -0
  69. package/src/lib/react/use-pointer.ts +515 -0
  70. package/src/lib/svelte/index.ts +10 -0
  71. package/src/lib/svelte/use-pointer.ts +507 -0
@@ -0,0 +1,515 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import {
3
+ createCurrentWritable as currentWritable,
4
+ type CurrentReadable
5
+ } from '../core/current-value.js';
6
+ import {
7
+ createInitialPointerState,
8
+ getPointerCoordinates,
9
+ getPointerNowSeconds,
10
+ normalizePointerKind,
11
+ resolvePointerFrameRequestMode,
12
+ type PointerClick,
13
+ type PointerFrameRequestMode,
14
+ type PointerState,
15
+ type PointerVec2
16
+ } from '../core/pointer.js';
17
+ import { useMotionGPU } from './motiongpu-context.js';
18
+
19
+ export type {
20
+ PointerClick,
21
+ PointerFrameRequestMode,
22
+ PointerKind,
23
+ PointerPoint,
24
+ PointerState
25
+ } from '../core/pointer.js';
26
+
27
+ /**
28
+ * Configuration for pointer input handling in `usePointer`.
29
+ */
30
+ export interface UsePointerOptions {
31
+ /**
32
+ * Enables pointer listeners.
33
+ *
34
+ * @default true
35
+ */
36
+ enabled?: boolean;
37
+ /**
38
+ * Frame wake-up strategy for pointer-driven state changes.
39
+ *
40
+ * @default 'auto'
41
+ */
42
+ requestFrame?: PointerFrameRequestMode;
43
+ /**
44
+ * Requests pointer capture on pointer down.
45
+ *
46
+ * @default true
47
+ */
48
+ capturePointer?: boolean;
49
+ /**
50
+ * Tracks pointer move/up outside canvas while pointer is pressed.
51
+ *
52
+ * @default true
53
+ */
54
+ trackWhilePressedOutsideCanvas?: boolean;
55
+ /**
56
+ * Enables click/tap synthesis on pointer up.
57
+ *
58
+ * @default true
59
+ */
60
+ clickEnabled?: boolean;
61
+ /**
62
+ * Maximum press duration to consider pointer up a click (milliseconds).
63
+ *
64
+ * @default 350
65
+ */
66
+ clickMaxDurationMs?: number;
67
+ /**
68
+ * Maximum pointer travel from down to up to consider pointer up a click (pixels).
69
+ *
70
+ * @default 8
71
+ */
72
+ clickMaxMovePx?: number;
73
+ /**
74
+ * Allowed pointer buttons for click synthesis.
75
+ *
76
+ * @default [0]
77
+ */
78
+ clickButtons?: number[];
79
+ /**
80
+ * Called after pointer move state update.
81
+ */
82
+ onMove?: (state: PointerState, event: PointerEvent) => void;
83
+ /**
84
+ * Called after pointer down state update.
85
+ */
86
+ onDown?: (state: PointerState, event: PointerEvent) => void;
87
+ /**
88
+ * Called after pointer up/cancel state update.
89
+ */
90
+ onUp?: (state: PointerState, event: PointerEvent) => void;
91
+ /**
92
+ * Called when click/tap is synthesized.
93
+ */
94
+ onClick?: (click: PointerClick, state: PointerState, event: PointerEvent) => void;
95
+ }
96
+
97
+ /**
98
+ * Reactive state returned by `usePointer`.
99
+ */
100
+ export interface UsePointerResult {
101
+ /**
102
+ * Current pointer state.
103
+ */
104
+ state: CurrentReadable<PointerState>;
105
+ /**
106
+ * Last synthesized click/tap event.
107
+ */
108
+ lastClick: CurrentReadable<PointerClick | null>;
109
+ /**
110
+ * Clears last click snapshot.
111
+ */
112
+ resetClick: () => void;
113
+ }
114
+
115
+ interface PointerDownSnapshot {
116
+ button: number;
117
+ inside: boolean;
118
+ pointerId: number;
119
+ pointerType: 'mouse' | 'pen' | 'touch';
120
+ px: PointerVec2;
121
+ timeMs: number;
122
+ uv: PointerVec2;
123
+ }
124
+
125
+ /**
126
+ * Resolves a valid click duration threshold in milliseconds.
127
+ */
128
+ function resolveClickMaxDurationMs(value: number | undefined): number {
129
+ if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) {
130
+ return 350;
131
+ }
132
+
133
+ return value;
134
+ }
135
+
136
+ /**
137
+ * Resolves a valid click travel threshold in pixels.
138
+ */
139
+ function resolveClickMaxMovePx(value: number | undefined): number {
140
+ if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
141
+ return 8;
142
+ }
143
+
144
+ return value;
145
+ }
146
+
147
+ /**
148
+ * Normalizes click button configuration with a primary-button fallback.
149
+ */
150
+ function normalizeClickButtons(buttons: number[] | undefined): Set<number> {
151
+ const source = buttons && buttons.length > 0 ? buttons : [0];
152
+ return new Set(source);
153
+ }
154
+
155
+ /**
156
+ * Tracks normalized pointer coordinates and click/tap snapshots for the active `FragCanvas`.
157
+ */
158
+ export function usePointer(options: UsePointerOptions = {}): UsePointerResult {
159
+ const motiongpu = useMotionGPU();
160
+ const stateRef = useRef(currentWritable<PointerState>(createInitialPointerState()));
161
+ const clickRef = useRef(currentWritable<PointerClick | null>(null));
162
+ const optionsRef = useRef(options);
163
+ const activePointerIdRef = useRef<number | null>(null);
164
+ const downSnapshotRef = useRef<PointerDownSnapshot | null>(null);
165
+ const clickCounterRef = useRef(0);
166
+ const previousPxRef = useRef<PointerVec2 | null>(null);
167
+ const previousUvRef = useRef<PointerVec2 | null>(null);
168
+ const previousTimeSecondsRef = useRef(0);
169
+
170
+ optionsRef.current = options;
171
+
172
+ const requestFrame = useCallback((): void => {
173
+ const mode = resolvePointerFrameRequestMode(
174
+ optionsRef.current.requestFrame ?? 'auto',
175
+ motiongpu.renderMode.current
176
+ );
177
+ if (mode === 'invalidate') {
178
+ motiongpu.invalidate();
179
+ return;
180
+ }
181
+ if (mode === 'advance') {
182
+ motiongpu.advance();
183
+ }
184
+ }, [motiongpu]);
185
+
186
+ /**
187
+ * Commits a full pointer state snapshot with computed delta and velocity vectors.
188
+ */
189
+ const updatePointerState = useCallback(
190
+ (input: {
191
+ button: number | null;
192
+ buttons: number;
193
+ downPx: PointerVec2 | null;
194
+ downUv: PointerVec2 | null;
195
+ dragging: boolean;
196
+ inside: boolean;
197
+ pointerId: number | null;
198
+ pointerType: 'mouse' | 'pen' | 'touch' | null;
199
+ pressed: boolean;
200
+ point: {
201
+ ndc: PointerVec2;
202
+ px: PointerVec2;
203
+ uv: PointerVec2;
204
+ };
205
+ resetDelta?: boolean;
206
+ }): PointerState => {
207
+ const nowSeconds = getPointerNowSeconds();
208
+ const previousTimeSeconds = previousTimeSecondsRef.current;
209
+ const dt = previousTimeSeconds > 0 ? Math.max(nowSeconds - previousTimeSeconds, 1e-6) : 0;
210
+ const previousPx = previousPxRef.current;
211
+ const previousUv = previousUvRef.current;
212
+ const deltaPx: PointerVec2 =
213
+ input.resetDelta || !previousPx
214
+ ? [0, 0]
215
+ : [input.point.px[0] - previousPx[0], input.point.px[1] - previousPx[1]];
216
+ const deltaUv: PointerVec2 =
217
+ input.resetDelta || !previousUv
218
+ ? [0, 0]
219
+ : [input.point.uv[0] - previousUv[0], input.point.uv[1] - previousUv[1]];
220
+ const velocityPx: PointerVec2 = dt > 0 ? [deltaPx[0] / dt, deltaPx[1] / dt] : [0, 0];
221
+ const velocityUv: PointerVec2 = dt > 0 ? [deltaUv[0] / dt, deltaUv[1] / dt] : [0, 0];
222
+ const nextState: PointerState = {
223
+ px: input.point.px,
224
+ uv: input.point.uv,
225
+ ndc: input.point.ndc,
226
+ inside: input.inside,
227
+ pressed: input.pressed,
228
+ dragging: input.dragging,
229
+ pointerType: input.pointerType,
230
+ pointerId: input.pointerId,
231
+ button: input.button,
232
+ buttons: input.buttons,
233
+ time: nowSeconds,
234
+ downPx: input.downPx,
235
+ downUv: input.downUv,
236
+ deltaPx,
237
+ deltaUv,
238
+ velocityPx,
239
+ velocityUv
240
+ };
241
+ stateRef.current.set(nextState);
242
+ previousPxRef.current = input.point.px;
243
+ previousUvRef.current = input.point.uv;
244
+ previousTimeSecondsRef.current = nowSeconds;
245
+ requestFrame();
246
+ return nextState;
247
+ },
248
+ [requestFrame]
249
+ );
250
+
251
+ useEffect(() => {
252
+ const enabled = optionsRef.current.enabled ?? true;
253
+ if (!enabled) {
254
+ return;
255
+ }
256
+
257
+ const canvas = motiongpu.canvas;
258
+ if (!canvas) {
259
+ return;
260
+ }
261
+
262
+ const isTrackedPointer = (event: PointerEvent): boolean =>
263
+ activePointerIdRef.current === null || event.pointerId === activePointerIdRef.current;
264
+
265
+ const handlePointerDown = (event: PointerEvent): void => {
266
+ const point = getPointerCoordinates(
267
+ event.clientX,
268
+ event.clientY,
269
+ canvas.getBoundingClientRect()
270
+ );
271
+ const pointerType = normalizePointerKind(event.pointerType);
272
+ activePointerIdRef.current = event.pointerId;
273
+ downSnapshotRef.current = {
274
+ pointerId: event.pointerId,
275
+ pointerType,
276
+ button: event.button,
277
+ timeMs: getPointerNowSeconds() * 1000,
278
+ px: point.px,
279
+ uv: point.uv,
280
+ inside: point.inside
281
+ };
282
+ if (optionsRef.current.capturePointer ?? true) {
283
+ try {
284
+ canvas.setPointerCapture(event.pointerId);
285
+ } catch {
286
+ // Browser rejected capture (e.g. unsupported pointer state).
287
+ }
288
+ }
289
+ const nextState = updatePointerState({
290
+ point,
291
+ inside: point.inside,
292
+ pressed: true,
293
+ dragging: false,
294
+ pointerType,
295
+ pointerId: event.pointerId,
296
+ button: event.button,
297
+ buttons: event.buttons,
298
+ downPx: point.px,
299
+ downUv: point.uv,
300
+ resetDelta: true
301
+ });
302
+ optionsRef.current.onDown?.(nextState, event);
303
+ };
304
+
305
+ const handleMove = (event: PointerEvent): void => {
306
+ if (!isTrackedPointer(event)) {
307
+ return;
308
+ }
309
+
310
+ const point = getPointerCoordinates(
311
+ event.clientX,
312
+ event.clientY,
313
+ canvas.getBoundingClientRect()
314
+ );
315
+ const pressed =
316
+ activePointerIdRef.current !== null && event.pointerId === activePointerIdRef.current;
317
+ const downPx = pressed ? (downSnapshotRef.current?.px ?? point.px) : null;
318
+ const downUv = pressed ? (downSnapshotRef.current?.uv ?? point.uv) : null;
319
+ let dragging = false;
320
+ if (pressed && downPx) {
321
+ const dx = point.px[0] - downPx[0];
322
+ const dy = point.px[1] - downPx[1];
323
+ dragging = Math.hypot(dx, dy) > 0;
324
+ }
325
+ const nextState = updatePointerState({
326
+ point,
327
+ inside: point.inside,
328
+ pressed,
329
+ dragging,
330
+ pointerType: normalizePointerKind(event.pointerType),
331
+ pointerId: event.pointerId,
332
+ button: pressed ? (downSnapshotRef.current?.button ?? event.button) : null,
333
+ buttons: event.buttons,
334
+ downPx,
335
+ downUv
336
+ });
337
+ optionsRef.current.onMove?.(nextState, event);
338
+ };
339
+
340
+ const handleWindowMove = (event: PointerEvent): void => {
341
+ if (
342
+ !(optionsRef.current.trackWhilePressedOutsideCanvas ?? true) ||
343
+ activePointerIdRef.current === null ||
344
+ event.pointerId !== activePointerIdRef.current
345
+ ) {
346
+ return;
347
+ }
348
+
349
+ const point = getPointerCoordinates(
350
+ event.clientX,
351
+ event.clientY,
352
+ canvas.getBoundingClientRect()
353
+ );
354
+ if (point.inside) {
355
+ return;
356
+ }
357
+
358
+ const downPx = downSnapshotRef.current?.px ?? point.px;
359
+ const downUv = downSnapshotRef.current?.uv ?? point.uv;
360
+ const dx = point.px[0] - downPx[0];
361
+ const dy = point.px[1] - downPx[1];
362
+ const nextState = updatePointerState({
363
+ point,
364
+ inside: false,
365
+ pressed: true,
366
+ dragging: Math.hypot(dx, dy) > 0,
367
+ pointerType:
368
+ downSnapshotRef.current?.pointerType ?? normalizePointerKind(event.pointerType),
369
+ pointerId: event.pointerId,
370
+ button: downSnapshotRef.current?.button ?? event.button,
371
+ buttons: event.buttons,
372
+ downPx,
373
+ downUv
374
+ });
375
+ optionsRef.current.onMove?.(nextState, event);
376
+ };
377
+
378
+ const releasePointer = (event: PointerEvent, emitClick: boolean): void => {
379
+ if (activePointerIdRef.current === null || event.pointerId !== activePointerIdRef.current) {
380
+ return;
381
+ }
382
+
383
+ const point = getPointerCoordinates(
384
+ event.clientX,
385
+ event.clientY,
386
+ canvas.getBoundingClientRect()
387
+ );
388
+ const previous = downSnapshotRef.current;
389
+ const pointerType = previous?.pointerType ?? normalizePointerKind(event.pointerType);
390
+ const nextState = updatePointerState({
391
+ point,
392
+ inside: point.inside,
393
+ pressed: false,
394
+ dragging: false,
395
+ pointerType,
396
+ pointerId: null,
397
+ button: null,
398
+ buttons: event.buttons,
399
+ downPx: null,
400
+ downUv: null
401
+ });
402
+ optionsRef.current.onUp?.(nextState, event);
403
+
404
+ if (
405
+ (optionsRef.current.capturePointer ?? true) &&
406
+ canvas.hasPointerCapture(event.pointerId)
407
+ ) {
408
+ try {
409
+ canvas.releasePointerCapture(event.pointerId);
410
+ } catch {
411
+ // Browser rejected release for this pointer id.
412
+ }
413
+ }
414
+
415
+ if (emitClick && (optionsRef.current.clickEnabled ?? true) && previous) {
416
+ const allowedButtons = normalizeClickButtons(optionsRef.current.clickButtons);
417
+ if (allowedButtons.has(previous.button)) {
418
+ const clickMaxDurationMs = resolveClickMaxDurationMs(
419
+ optionsRef.current.clickMaxDurationMs
420
+ );
421
+ const clickMaxMovePx = resolveClickMaxMovePx(optionsRef.current.clickMaxMovePx);
422
+ const durationMs = getPointerNowSeconds() * 1000 - previous.timeMs;
423
+ const dx = point.px[0] - previous.px[0];
424
+ const dy = point.px[1] - previous.px[1];
425
+ const moveDistance = Math.hypot(dx, dy);
426
+ if (
427
+ previous.inside &&
428
+ point.inside &&
429
+ durationMs <= clickMaxDurationMs &&
430
+ moveDistance <= clickMaxMovePx
431
+ ) {
432
+ clickCounterRef.current += 1;
433
+ const click: PointerClick = {
434
+ id: clickCounterRef.current,
435
+ time: getPointerNowSeconds(),
436
+ pointerType,
437
+ pointerId: event.pointerId,
438
+ button: previous.button,
439
+ modifiers: {
440
+ alt: event.altKey,
441
+ ctrl: event.ctrlKey,
442
+ shift: event.shiftKey,
443
+ meta: event.metaKey
444
+ },
445
+ px: point.px,
446
+ uv: point.uv,
447
+ ndc: point.ndc
448
+ };
449
+ clickRef.current.set(click);
450
+ optionsRef.current.onClick?.(click, nextState, event);
451
+ requestFrame();
452
+ }
453
+ }
454
+ }
455
+
456
+ activePointerIdRef.current = null;
457
+ downSnapshotRef.current = null;
458
+ };
459
+
460
+ const handlePointerUp = (event: PointerEvent): void => {
461
+ releasePointer(event, true);
462
+ };
463
+
464
+ const handlePointerCancel = (event: PointerEvent): void => {
465
+ releasePointer(event, false);
466
+ };
467
+
468
+ const handlePointerLeave = (): void => {
469
+ if (activePointerIdRef.current !== null) {
470
+ return;
471
+ }
472
+ const current = stateRef.current.current;
473
+ stateRef.current.set({
474
+ ...current,
475
+ inside: false,
476
+ time: getPointerNowSeconds(),
477
+ deltaPx: [0, 0],
478
+ deltaUv: [0, 0],
479
+ velocityPx: [0, 0],
480
+ velocityUv: [0, 0]
481
+ });
482
+ requestFrame();
483
+ };
484
+
485
+ canvas.addEventListener('pointerdown', handlePointerDown);
486
+ canvas.addEventListener('pointermove', handleMove);
487
+ canvas.addEventListener('pointerup', handlePointerUp);
488
+ canvas.addEventListener('pointercancel', handlePointerCancel);
489
+ canvas.addEventListener('pointerleave', handlePointerLeave);
490
+ if (optionsRef.current.trackWhilePressedOutsideCanvas ?? true) {
491
+ window.addEventListener('pointermove', handleWindowMove);
492
+ window.addEventListener('pointerup', handlePointerUp);
493
+ window.addEventListener('pointercancel', handlePointerCancel);
494
+ }
495
+
496
+ return () => {
497
+ canvas.removeEventListener('pointerdown', handlePointerDown);
498
+ canvas.removeEventListener('pointermove', handleMove);
499
+ canvas.removeEventListener('pointerup', handlePointerUp);
500
+ canvas.removeEventListener('pointercancel', handlePointerCancel);
501
+ canvas.removeEventListener('pointerleave', handlePointerLeave);
502
+ window.removeEventListener('pointermove', handleWindowMove);
503
+ window.removeEventListener('pointerup', handlePointerUp);
504
+ window.removeEventListener('pointercancel', handlePointerCancel);
505
+ };
506
+ }, [motiongpu, requestFrame, updatePointerState]);
507
+
508
+ return {
509
+ state: stateRef.current,
510
+ lastClick: clickRef.current,
511
+ resetClick: useCallback(() => {
512
+ clickRef.current.set(null);
513
+ }, [])
514
+ };
515
+ }
@@ -12,6 +12,7 @@ export {
12
12
  } from '../passes/index.js';
13
13
  export { useMotionGPU } from './motiongpu-context.js';
14
14
  export { useFrame } from './frame-context.js';
15
+ export { usePointer } from './use-pointer.js';
15
16
  export { useTexture } from './use-texture.js';
16
17
  export type {
17
18
  FrameInvalidationToken,
@@ -56,6 +57,15 @@ export type {
56
57
  } from '../core/material.js';
57
58
  export type { MotionGPUContext } from './motiongpu-context.js';
58
59
  export type { UseFrameOptions, UseFrameResult } from './frame-context.js';
60
+ export type {
61
+ PointerClick,
62
+ PointerFrameRequestMode,
63
+ PointerKind,
64
+ PointerPoint,
65
+ PointerState,
66
+ UsePointerOptions,
67
+ UsePointerResult
68
+ } from './use-pointer.js';
59
69
  export type { TextureUrlInput, UseTextureResult } from './use-texture.js';
60
70
  export type {
61
71
  StorageBufferAccess,