@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.
- package/README.md +35 -2
- package/dist/core/compute-bindgroup-cache.d.ts +13 -0
- package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
- package/dist/core/compute-bindgroup-cache.js +45 -0
- package/dist/core/compute-bindgroup-cache.js.map +1 -0
- package/dist/core/compute-shader.d.ts +48 -0
- package/dist/core/compute-shader.d.ts.map +1 -1
- package/dist/core/compute-shader.js +34 -1
- package/dist/core/compute-shader.js.map +1 -1
- package/dist/core/error-diagnostics.d.ts +8 -1
- package/dist/core/error-diagnostics.d.ts.map +1 -1
- package/dist/core/error-diagnostics.js +7 -3
- package/dist/core/error-diagnostics.js.map +1 -1
- package/dist/core/error-report.d.ts.map +1 -1
- package/dist/core/error-report.js +19 -1
- package/dist/core/error-report.js.map +1 -1
- package/dist/core/material.d.ts.map +1 -1
- package/dist/core/material.js +2 -1
- package/dist/core/material.js.map +1 -1
- package/dist/core/pointer.d.ts +96 -0
- package/dist/core/pointer.d.ts.map +1 -0
- package/dist/core/pointer.js +71 -0
- package/dist/core/pointer.js.map +1 -0
- package/dist/core/renderer.d.ts.map +1 -1
- package/dist/core/renderer.js +150 -85
- package/dist/core/renderer.js.map +1 -1
- package/dist/core/runtime-loop.d.ts.map +1 -1
- package/dist/core/runtime-loop.js +26 -14
- package/dist/core/runtime-loop.js.map +1 -1
- package/dist/core/shader.d.ts +7 -2
- package/dist/core/shader.d.ts.map +1 -1
- package/dist/core/shader.js +1 -0
- package/dist/core/shader.js.map +1 -1
- package/dist/core/textures.d.ts +4 -0
- package/dist/core/textures.d.ts.map +1 -1
- package/dist/core/textures.js +2 -1
- package/dist/core/textures.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/advanced.js +2 -1
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -1
- package/dist/react/use-pointer.d.ts +94 -0
- package/dist/react/use-pointer.d.ts.map +1 -0
- package/dist/react/use-pointer.js +285 -0
- package/dist/react/use-pointer.js.map +1 -0
- package/dist/svelte/advanced.js +2 -1
- package/dist/svelte/index.d.ts +2 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +2 -1
- package/dist/svelte/use-pointer.d.ts +94 -0
- package/dist/svelte/use-pointer.d.ts.map +1 -0
- package/dist/svelte/use-pointer.js +292 -0
- package/dist/svelte/use-pointer.js.map +1 -0
- package/package.json +1 -1
- package/src/lib/core/compute-bindgroup-cache.ts +73 -0
- package/src/lib/core/compute-shader.ts +86 -0
- package/src/lib/core/error-diagnostics.ts +29 -4
- package/src/lib/core/error-report.ts +26 -1
- package/src/lib/core/material.ts +2 -1
- package/src/lib/core/pointer.ts +177 -0
- package/src/lib/core/renderer.ts +198 -92
- package/src/lib/core/runtime-loop.ts +37 -16
- package/src/lib/core/shader.ts +13 -2
- package/src/lib/core/textures.ts +6 -1
- package/src/lib/core/types.ts +1 -1
- package/src/lib/react/index.ts +10 -0
- package/src/lib/react/use-pointer.ts +515 -0
- package/src/lib/svelte/index.ts +10 -0
- 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
|
+
}
|
package/src/lib/svelte/index.ts
CHANGED
|
@@ -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,
|