@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,507 @@
1
+ import { onMount } from 'svelte';
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
+ * Normalizes click button configuration with a primary-button fallback.
127
+ */
128
+ function normalizeClickButtons(buttons: number[] | undefined): Set<number> {
129
+ const source = buttons && buttons.length > 0 ? buttons : [0];
130
+ return new Set(source);
131
+ }
132
+
133
+ /**
134
+ * Resolves a valid click duration threshold in milliseconds.
135
+ */
136
+ function resolveClickMaxDurationMs(value: number | undefined): number {
137
+ if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) {
138
+ return 350;
139
+ }
140
+
141
+ return value;
142
+ }
143
+
144
+ /**
145
+ * Resolves a valid click travel threshold in pixels.
146
+ */
147
+ function resolveClickMaxMovePx(value: number | undefined): number {
148
+ if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
149
+ return 8;
150
+ }
151
+
152
+ return value;
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 pointerState = currentWritable<PointerState>(createInitialPointerState());
161
+ const lastClick = currentWritable<PointerClick | null>(null);
162
+ const enabled = options.enabled ?? true;
163
+ const requestFrameMode = options.requestFrame ?? 'auto';
164
+ const capturePointer = options.capturePointer ?? true;
165
+ const trackOutside = options.trackWhilePressedOutsideCanvas ?? true;
166
+ const clickEnabled = options.clickEnabled ?? true;
167
+ const clickMaxDurationMs = resolveClickMaxDurationMs(options.clickMaxDurationMs);
168
+ const clickMaxMovePx = resolveClickMaxMovePx(options.clickMaxMovePx);
169
+ const clickButtons = normalizeClickButtons(options.clickButtons);
170
+ let activePointerId: number | null = null;
171
+ let downSnapshot: PointerDownSnapshot | null = null;
172
+ let clickCounter = 0;
173
+ let previousPx: PointerVec2 | null = null;
174
+ let previousUv: PointerVec2 | null = null;
175
+ let previousTimeSeconds = 0;
176
+
177
+ const requestFrame = (): void => {
178
+ const mode = resolvePointerFrameRequestMode(requestFrameMode, motiongpu.renderMode.current);
179
+ if (mode === 'invalidate') {
180
+ motiongpu.invalidate();
181
+ return;
182
+ }
183
+ if (mode === 'advance') {
184
+ motiongpu.advance();
185
+ }
186
+ };
187
+
188
+ /**
189
+ * Commits a full pointer state snapshot with computed delta and velocity vectors.
190
+ */
191
+ const updatePointerState = (input: {
192
+ button: number | null;
193
+ buttons: number;
194
+ downPx: PointerVec2 | null;
195
+ downUv: PointerVec2 | null;
196
+ dragging: boolean;
197
+ inside: boolean;
198
+ pointerId: number | null;
199
+ pointerType: 'mouse' | 'pen' | 'touch' | null;
200
+ pressed: boolean;
201
+ point: {
202
+ ndc: PointerVec2;
203
+ px: PointerVec2;
204
+ uv: PointerVec2;
205
+ };
206
+ resetDelta?: boolean;
207
+ }): PointerState => {
208
+ const nowSeconds = getPointerNowSeconds();
209
+ const dt = previousTimeSeconds > 0 ? Math.max(nowSeconds - previousTimeSeconds, 1e-6) : 0;
210
+ const deltaPx: PointerVec2 =
211
+ input.resetDelta || !previousPx
212
+ ? [0, 0]
213
+ : [input.point.px[0] - previousPx[0], input.point.px[1] - previousPx[1]];
214
+ const deltaUv: PointerVec2 =
215
+ input.resetDelta || !previousUv
216
+ ? [0, 0]
217
+ : [input.point.uv[0] - previousUv[0], input.point.uv[1] - previousUv[1]];
218
+ const velocityPx: PointerVec2 = dt > 0 ? [deltaPx[0] / dt, deltaPx[1] / dt] : [0, 0];
219
+ const velocityUv: PointerVec2 = dt > 0 ? [deltaUv[0] / dt, deltaUv[1] / dt] : [0, 0];
220
+ const nextState: PointerState = {
221
+ px: input.point.px,
222
+ uv: input.point.uv,
223
+ ndc: input.point.ndc,
224
+ inside: input.inside,
225
+ pressed: input.pressed,
226
+ dragging: input.dragging,
227
+ pointerType: input.pointerType,
228
+ pointerId: input.pointerId,
229
+ button: input.button,
230
+ buttons: input.buttons,
231
+ time: nowSeconds,
232
+ downPx: input.downPx,
233
+ downUv: input.downUv,
234
+ deltaPx,
235
+ deltaUv,
236
+ velocityPx,
237
+ velocityUv
238
+ };
239
+ pointerState.set(nextState);
240
+ previousPx = input.point.px;
241
+ previousUv = input.point.uv;
242
+ previousTimeSeconds = nowSeconds;
243
+ requestFrame();
244
+ return nextState;
245
+ };
246
+
247
+ /**
248
+ * Updates only the `inside` flag while keeping the latest pointer coordinates.
249
+ */
250
+ const updateInsideState = (inside: boolean): PointerState => {
251
+ const current = pointerState.current;
252
+ const nextState: PointerState = {
253
+ ...current,
254
+ inside,
255
+ time: getPointerNowSeconds(),
256
+ deltaPx: [0, 0],
257
+ deltaUv: [0, 0],
258
+ velocityPx: [0, 0],
259
+ velocityUv: [0, 0]
260
+ };
261
+ pointerState.set(nextState);
262
+ requestFrame();
263
+ return nextState;
264
+ };
265
+
266
+ /**
267
+ * Checks whether an event belongs to the active tracked pointer.
268
+ */
269
+ const isTrackedPointer = (event: PointerEvent): boolean =>
270
+ activePointerId === null || event.pointerId === activePointerId;
271
+
272
+ onMount(() => {
273
+ if (!enabled) {
274
+ return;
275
+ }
276
+
277
+ const canvas = motiongpu.canvas;
278
+ if (!canvas) {
279
+ return;
280
+ }
281
+
282
+ const handlePointerDown = (event: PointerEvent): void => {
283
+ const point = getPointerCoordinates(
284
+ event.clientX,
285
+ event.clientY,
286
+ canvas.getBoundingClientRect()
287
+ );
288
+ const pointerType = normalizePointerKind(event.pointerType);
289
+ activePointerId = event.pointerId;
290
+ downSnapshot = {
291
+ pointerId: event.pointerId,
292
+ pointerType,
293
+ button: event.button,
294
+ timeMs: getPointerNowSeconds() * 1000,
295
+ px: point.px,
296
+ uv: point.uv,
297
+ inside: point.inside
298
+ };
299
+ if (capturePointer) {
300
+ try {
301
+ canvas.setPointerCapture(event.pointerId);
302
+ } catch {
303
+ // Browser rejected capture (e.g. unsupported pointer state).
304
+ }
305
+ }
306
+ const nextState = updatePointerState({
307
+ point,
308
+ inside: point.inside,
309
+ pressed: true,
310
+ dragging: false,
311
+ pointerType,
312
+ pointerId: event.pointerId,
313
+ button: event.button,
314
+ buttons: event.buttons,
315
+ downPx: point.px,
316
+ downUv: point.uv,
317
+ resetDelta: true
318
+ });
319
+ options.onDown?.(nextState, event);
320
+ };
321
+
322
+ const handleMove = (event: PointerEvent): void => {
323
+ if (!isTrackedPointer(event)) {
324
+ return;
325
+ }
326
+
327
+ const point = getPointerCoordinates(
328
+ event.clientX,
329
+ event.clientY,
330
+ canvas.getBoundingClientRect()
331
+ );
332
+ const pressed = activePointerId !== null && event.pointerId === activePointerId;
333
+ const downPx = pressed ? (downSnapshot?.px ?? point.px) : null;
334
+ const downUv = pressed ? (downSnapshot?.uv ?? point.uv) : null;
335
+ let dragging = false;
336
+ if (pressed && downPx) {
337
+ const dx = point.px[0] - downPx[0];
338
+ const dy = point.px[1] - downPx[1];
339
+ dragging = Math.hypot(dx, dy) > 0;
340
+ }
341
+ const nextState = updatePointerState({
342
+ point,
343
+ inside: point.inside,
344
+ pressed,
345
+ dragging,
346
+ pointerType: normalizePointerKind(event.pointerType),
347
+ pointerId: event.pointerId,
348
+ button: pressed ? (downSnapshot?.button ?? event.button) : null,
349
+ buttons: event.buttons,
350
+ downPx,
351
+ downUv
352
+ });
353
+ options.onMove?.(nextState, event);
354
+ };
355
+
356
+ const handleWindowMove = (event: PointerEvent): void => {
357
+ if (!trackOutside || activePointerId === null || event.pointerId !== activePointerId) {
358
+ return;
359
+ }
360
+
361
+ const point = getPointerCoordinates(
362
+ event.clientX,
363
+ event.clientY,
364
+ canvas.getBoundingClientRect()
365
+ );
366
+ if (point.inside) {
367
+ return;
368
+ }
369
+ const downPx = downSnapshot?.px ?? point.px;
370
+ const downUv = downSnapshot?.uv ?? point.uv;
371
+ const dx = point.px[0] - downPx[0];
372
+ const dy = point.px[1] - downPx[1];
373
+ const nextState = updatePointerState({
374
+ point,
375
+ inside: false,
376
+ pressed: true,
377
+ dragging: Math.hypot(dx, dy) > 0,
378
+ pointerType: downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType),
379
+ pointerId: event.pointerId,
380
+ button: downSnapshot?.button ?? event.button,
381
+ buttons: event.buttons,
382
+ downPx,
383
+ downUv
384
+ });
385
+ options.onMove?.(nextState, event);
386
+ };
387
+
388
+ const releasePointer = (event: PointerEvent, emitClick: boolean): void => {
389
+ if (activePointerId === null || event.pointerId !== activePointerId) {
390
+ return;
391
+ }
392
+
393
+ const point = getPointerCoordinates(
394
+ event.clientX,
395
+ event.clientY,
396
+ canvas.getBoundingClientRect()
397
+ );
398
+ const pointerType = downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType);
399
+ const previous = downSnapshot;
400
+ const nextState = updatePointerState({
401
+ point,
402
+ inside: point.inside,
403
+ pressed: false,
404
+ dragging: false,
405
+ pointerType,
406
+ pointerId: null,
407
+ button: null,
408
+ buttons: event.buttons,
409
+ downPx: null,
410
+ downUv: null
411
+ });
412
+ options.onUp?.(nextState, event);
413
+
414
+ if (capturePointer && canvas.hasPointerCapture(event.pointerId)) {
415
+ try {
416
+ canvas.releasePointerCapture(event.pointerId);
417
+ } catch {
418
+ // Browser rejected release for this pointer id.
419
+ }
420
+ }
421
+
422
+ if (emitClick && clickEnabled && previous && clickButtons.has(previous.button)) {
423
+ const durationMs = getPointerNowSeconds() * 1000 - previous.timeMs;
424
+ const dx = point.px[0] - previous.px[0];
425
+ const dy = point.px[1] - previous.px[1];
426
+ const moveDistance = Math.hypot(dx, dy);
427
+ if (
428
+ previous.inside &&
429
+ point.inside &&
430
+ durationMs <= clickMaxDurationMs &&
431
+ moveDistance <= clickMaxMovePx
432
+ ) {
433
+ clickCounter += 1;
434
+ const click: PointerClick = {
435
+ id: clickCounter,
436
+ time: getPointerNowSeconds(),
437
+ pointerType,
438
+ pointerId: event.pointerId,
439
+ button: previous.button,
440
+ modifiers: {
441
+ alt: event.altKey,
442
+ ctrl: event.ctrlKey,
443
+ shift: event.shiftKey,
444
+ meta: event.metaKey
445
+ },
446
+ px: point.px,
447
+ uv: point.uv,
448
+ ndc: point.ndc
449
+ };
450
+ lastClick.set(click);
451
+ options.onClick?.(click, nextState, event);
452
+ requestFrame();
453
+ }
454
+ }
455
+
456
+ activePointerId = null;
457
+ downSnapshot = 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 (activePointerId !== null) {
470
+ return;
471
+ }
472
+ updateInsideState(false);
473
+ };
474
+
475
+ canvas.addEventListener('pointerdown', handlePointerDown);
476
+ canvas.addEventListener('pointermove', handleMove);
477
+ canvas.addEventListener('pointerup', handlePointerUp);
478
+ canvas.addEventListener('pointercancel', handlePointerCancel);
479
+ canvas.addEventListener('pointerleave', handlePointerLeave);
480
+ if (trackOutside) {
481
+ window.addEventListener('pointermove', handleWindowMove);
482
+ window.addEventListener('pointerup', handlePointerUp);
483
+ window.addEventListener('pointercancel', handlePointerCancel);
484
+ }
485
+
486
+ return () => {
487
+ canvas.removeEventListener('pointerdown', handlePointerDown);
488
+ canvas.removeEventListener('pointermove', handleMove);
489
+ canvas.removeEventListener('pointerup', handlePointerUp);
490
+ canvas.removeEventListener('pointercancel', handlePointerCancel);
491
+ canvas.removeEventListener('pointerleave', handlePointerLeave);
492
+ if (trackOutside) {
493
+ window.removeEventListener('pointermove', handleWindowMove);
494
+ window.removeEventListener('pointerup', handlePointerUp);
495
+ window.removeEventListener('pointercancel', handlePointerCancel);
496
+ }
497
+ };
498
+ });
499
+
500
+ return {
501
+ state: pointerState,
502
+ lastClick,
503
+ resetClick: () => {
504
+ lastClick.set(null);
505
+ }
506
+ };
507
+ }