@flare-engine/input 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,541 @@
1
+ // src/pointer.ts
2
+ function createPointer() {
3
+ return {
4
+ position: { x: 0, y: 0 },
5
+ delta: { x: 0, y: 0 },
6
+ isDown: false,
7
+ justPressed: false,
8
+ justReleased: false
9
+ };
10
+ }
11
+ function resetPointerFrame(pointer) {
12
+ pointer.delta.x = 0;
13
+ pointer.delta.y = 0;
14
+ pointer.justPressed = false;
15
+ pointer.justReleased = false;
16
+ }
17
+ function pointerDown(pointer, x, y) {
18
+ const wasDown = pointer.isDown;
19
+ pointer.isDown = true;
20
+ if (!wasDown) {
21
+ pointer.justPressed = true;
22
+ }
23
+ pointer.delta.x = x - pointer.position.x;
24
+ pointer.delta.y = y - pointer.position.y;
25
+ pointer.position.x = x;
26
+ pointer.position.y = y;
27
+ }
28
+ function pointerMove(pointer, x, y) {
29
+ pointer.delta.x = x - pointer.position.x;
30
+ pointer.delta.y = y - pointer.position.y;
31
+ pointer.position.x = x;
32
+ pointer.position.y = y;
33
+ }
34
+ function pointerUp(pointer, x, y) {
35
+ pointer.isDown = false;
36
+ pointer.justReleased = true;
37
+ pointer.delta.x = x - pointer.position.x;
38
+ pointer.delta.y = y - pointer.position.y;
39
+ pointer.position.x = x;
40
+ pointer.position.y = y;
41
+ }
42
+
43
+ // src/gamepad.ts
44
+ var GamepadButton = {
45
+ A: 0,
46
+ B: 1,
47
+ X: 2,
48
+ Y: 3,
49
+ LB: 4,
50
+ RB: 5,
51
+ LT: 6,
52
+ RT: 7,
53
+ Back: 8,
54
+ Start: 9,
55
+ LS: 10,
56
+ RS: 11,
57
+ DpadUp: 12,
58
+ DpadDown: 13,
59
+ DpadLeft: 14,
60
+ DpadRight: 15,
61
+ Home: 16
62
+ };
63
+ var GamepadAxis = {
64
+ LeftX: 0,
65
+ LeftY: 1,
66
+ RightX: 2,
67
+ RightY: 3
68
+ };
69
+ function createGamepadState(index = 0, buttonCount = 17, axisCount = 4) {
70
+ const buttons = new Array(buttonCount);
71
+ for (let i = 0; i < buttonCount; i++) {
72
+ buttons[i] = { pressed: false, value: 0, pressedThisFrame: false, releasedThisFrame: false };
73
+ }
74
+ const axes = new Array(axisCount).fill(0);
75
+ return { connected: false, id: "", index, buttons, axes, timestamp: 0 };
76
+ }
77
+ function applyDeadZone(value, deadZone) {
78
+ const abs = Math.abs(value);
79
+ if (abs <= deadZone) return 0;
80
+ const sign = value < 0 ? -1 : 1;
81
+ return sign * ((abs - deadZone) / (1 - deadZone));
82
+ }
83
+ function updateGamepadFromSnapshot(state, snapshot, deadZone = 0) {
84
+ if (snapshot === null) {
85
+ if (!state.connected && !anyButtonDown(state)) {
86
+ clearFrameFlags(state);
87
+ return;
88
+ }
89
+ for (let i = 0; i < state.buttons.length; i++) {
90
+ const b = state.buttons[i];
91
+ if (!b) continue;
92
+ b.pressedThisFrame = false;
93
+ b.releasedThisFrame = b.pressed;
94
+ b.pressed = false;
95
+ b.value = 0;
96
+ }
97
+ for (let i = 0; i < state.axes.length; i++) state.axes[i] = 0;
98
+ state.connected = false;
99
+ state.id = "";
100
+ return;
101
+ }
102
+ state.connected = true;
103
+ state.id = snapshot.id;
104
+ state.index = snapshot.index;
105
+ state.timestamp = snapshot.timestamp;
106
+ const srcButtons = snapshot.buttons;
107
+ const bLen = Math.min(state.buttons.length, srcButtons.length);
108
+ for (let i = 0; i < bLen; i++) {
109
+ const prev = state.buttons[i];
110
+ const src = srcButtons[i];
111
+ if (!prev || !src) continue;
112
+ const wasPressed = prev.pressed;
113
+ prev.pressed = src.pressed;
114
+ prev.value = src.value;
115
+ prev.pressedThisFrame = src.pressed && !wasPressed;
116
+ prev.releasedThisFrame = !src.pressed && wasPressed;
117
+ }
118
+ for (let i = bLen; i < state.buttons.length; i++) {
119
+ const b = state.buttons[i];
120
+ if (!b) continue;
121
+ b.pressedThisFrame = false;
122
+ b.releasedThisFrame = b.pressed;
123
+ b.pressed = false;
124
+ b.value = 0;
125
+ }
126
+ const aLen = Math.min(state.axes.length, snapshot.axes.length);
127
+ for (let i = 0; i < aLen; i++) {
128
+ state.axes[i] = applyDeadZone(snapshot.axes[i] ?? 0, deadZone);
129
+ }
130
+ for (let i = aLen; i < state.axes.length; i++) state.axes[i] = 0;
131
+ }
132
+ function resetGamepadFrame(state) {
133
+ clearFrameFlags(state);
134
+ }
135
+ function clearFrameFlags(state) {
136
+ for (let i = 0; i < state.buttons.length; i++) {
137
+ const b = state.buttons[i];
138
+ if (!b) continue;
139
+ b.pressedThisFrame = false;
140
+ b.releasedThisFrame = false;
141
+ }
142
+ }
143
+ function anyButtonDown(state) {
144
+ for (let i = 0; i < state.buttons.length; i++) {
145
+ if (state.buttons[i]?.pressed) return true;
146
+ }
147
+ return false;
148
+ }
149
+ function isButtonDown(state, button) {
150
+ return state.buttons[button]?.pressed ?? false;
151
+ }
152
+ function wasButtonPressed(state, button) {
153
+ return state.buttons[button]?.pressedThisFrame ?? false;
154
+ }
155
+ function wasButtonReleased(state, button) {
156
+ return state.buttons[button]?.releasedThisFrame ?? false;
157
+ }
158
+ function getAxis(state, axis) {
159
+ return state.axes[axis] ?? 0;
160
+ }
161
+
162
+ // src/remap.ts
163
+ function createIdentityRemap(buttonCount = 17, axisCount = 4) {
164
+ const buttons = new Array(buttonCount);
165
+ for (let i = 0; i < buttonCount; i++) buttons[i] = i;
166
+ const axes = new Array(axisCount);
167
+ for (let i = 0; i < axisCount; i++) axes[i] = { index: i, invert: false, scale: 1 };
168
+ return { buttons, axes };
169
+ }
170
+ function applyRemap(snapshot, table) {
171
+ const buttons = new Array(table.buttons.length);
172
+ for (let logical = 0; logical < table.buttons.length; logical++) {
173
+ const physical = table.buttons[logical] ?? logical;
174
+ const src = snapshot.buttons[physical];
175
+ buttons[logical] = { pressed: src?.pressed ?? false, value: src?.value ?? 0 };
176
+ }
177
+ const axes = new Array(table.axes.length);
178
+ for (let logical = 0; logical < table.axes.length; logical++) {
179
+ const map = table.axes[logical] ?? { index: logical, invert: false, scale: 1 };
180
+ const raw = snapshot.axes[map.index] ?? 0;
181
+ axes[logical] = raw * map.scale * (map.invert ? -1 : 1);
182
+ }
183
+ return { id: snapshot.id, index: snapshot.index, timestamp: snapshot.timestamp, buttons, axes };
184
+ }
185
+ async function saveRemap(store, key, table) {
186
+ await store.set(key, JSON.stringify(table));
187
+ }
188
+ async function loadRemap(store, key) {
189
+ const raw = await store.get(key);
190
+ if (raw === null) return null;
191
+ try {
192
+ const parsed = JSON.parse(raw);
193
+ if (!isRemapTable(parsed)) return null;
194
+ return parsed;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ function isRemapTable(value) {
200
+ if (typeof value !== "object" || value === null) return false;
201
+ const obj = value;
202
+ if (!Array.isArray(obj.buttons) || !Array.isArray(obj.axes)) return false;
203
+ for (const b of obj.buttons) if (typeof b !== "number") return false;
204
+ for (const a of obj.axes) {
205
+ if (typeof a !== "object" || a === null) return false;
206
+ const ax = a;
207
+ if (typeof ax.index !== "number" || typeof ax.invert !== "boolean" || typeof ax.scale !== "number") {
208
+ return false;
209
+ }
210
+ }
211
+ return true;
212
+ }
213
+
214
+ // src/input-manager.ts
215
+ var InputManager = class {
216
+ /** Primary pointer (first touch or mouse). */
217
+ pointer = createPointer();
218
+ /** All active touch points. */
219
+ touches = [];
220
+ _touchMap = /* @__PURE__ */ new Map();
221
+ _gamepads = [];
222
+ _gamepadDeadZone = 0.15;
223
+ _gamepadRemaps = /* @__PURE__ */ new Map();
224
+ /** Call once per frame to reset per-frame flags. */
225
+ update() {
226
+ resetPointerFrame(this.pointer);
227
+ for (let i = 0; i < this._gamepads.length; i++) {
228
+ const pad = this._gamepads[i];
229
+ if (pad) resetGamepadFrame(pad);
230
+ }
231
+ }
232
+ /** Feed a touch/pointer begin event. */
233
+ onPointerDown(id, x, y) {
234
+ pointerDown(this.pointer, x, y);
235
+ this._addOrUpdateTouch(id, x, y, true);
236
+ }
237
+ /** Feed a touch/pointer move event. */
238
+ onPointerMove(id, x, y) {
239
+ pointerMove(this.pointer, x, y);
240
+ this._addOrUpdateTouch(id, x, y, true);
241
+ }
242
+ /** Feed a touch/pointer end event. */
243
+ onPointerUp(id, x, y) {
244
+ pointerUp(this.pointer, x, y);
245
+ this._removeTouch(id);
246
+ }
247
+ /** Access the gamepad state at `index`. Allocates on first access. */
248
+ gamepad(index) {
249
+ let pad = this._gamepads[index];
250
+ if (!pad) {
251
+ pad = createGamepadState(index);
252
+ this._gamepads[index] = pad;
253
+ }
254
+ return pad;
255
+ }
256
+ /** Dead zone in [0, 1). Axes at or below this magnitude read as 0. */
257
+ setGamepadDeadZone(deadZone) {
258
+ this._gamepadDeadZone = Math.max(0, Math.min(0.95, deadZone));
259
+ }
260
+ /** Install a remap table for a specific gamepad index. Pass `null` to clear. */
261
+ setGamepadRemap(index, table) {
262
+ if (table === null) this._gamepadRemaps.delete(index);
263
+ else this._gamepadRemaps.set(index, table);
264
+ }
265
+ /** Poll all gamepads from the given source, applying remap + dead zones. */
266
+ pollGamepads(source) {
267
+ const snapshots = source.poll();
268
+ for (let i = 0; i < snapshots.length; i++) {
269
+ const pad = this.gamepad(i);
270
+ const snap = snapshots[i] ?? null;
271
+ const remapped = snap !== null ? this._gamepadRemaps.get(i) ? applyRemap(snap, this._gamepadRemaps.get(i)) : snap : null;
272
+ updateGamepadFromSnapshot(pad, remapped, this._gamepadDeadZone);
273
+ }
274
+ }
275
+ _addOrUpdateTouch(id, x, y, isDown) {
276
+ let touch = this._touchMap.get(id);
277
+ if (!touch) {
278
+ touch = { id, position: { x, y }, isDown };
279
+ this._touchMap.set(id, touch);
280
+ this.touches.push(touch);
281
+ } else {
282
+ touch.position.x = x;
283
+ touch.position.y = y;
284
+ touch.isDown = isDown;
285
+ }
286
+ }
287
+ _removeTouch(id) {
288
+ const touch = this._touchMap.get(id);
289
+ if (!touch) return;
290
+ this._touchMap.delete(id);
291
+ const idx = this.touches.indexOf(touch);
292
+ if (idx !== -1) {
293
+ this.touches.splice(idx, 1);
294
+ }
295
+ }
296
+ };
297
+
298
+ // src/gamepad-source.ts
299
+ function createWebGamepadSource(nav) {
300
+ const source = nav ?? (typeof globalThis === "undefined" ? void 0 : globalThis.navigator);
301
+ return {
302
+ poll() {
303
+ if (!source?.getGamepads) return [];
304
+ const pads = source.getGamepads();
305
+ const out = new Array(pads.length);
306
+ for (let i = 0; i < pads.length; i++) {
307
+ const p = pads[i];
308
+ if (!p || !p.connected) {
309
+ out[i] = null;
310
+ continue;
311
+ }
312
+ out[i] = webGamepadToSnapshot(p);
313
+ }
314
+ return out;
315
+ }
316
+ };
317
+ }
318
+ function webGamepadToSnapshot(p) {
319
+ const buttons = new Array(p.buttons.length);
320
+ for (let i = 0; i < p.buttons.length; i++) {
321
+ const b = p.buttons[i];
322
+ buttons[i] = { pressed: b?.pressed ?? false, value: b?.value ?? 0 };
323
+ }
324
+ const axes = new Array(p.axes.length);
325
+ for (let i = 0; i < p.axes.length; i++) axes[i] = p.axes[i] ?? 0;
326
+ return { id: p.id, index: p.index, timestamp: p.timestamp, buttons, axes };
327
+ }
328
+
329
+ // src/gestures.ts
330
+ import { Signal } from "@flare-engine/events";
331
+ var DEFAULT_TAP_MAX_DURATION = 0.3;
332
+ var DEFAULT_TAP_MAX_DISTANCE = 20;
333
+ var DEFAULT_LONG_PRESS_MIN_DURATION = 0.5;
334
+ var DEFAULT_SWIPE_MIN_DISTANCE = 50;
335
+ var DEFAULT_SWIPE_MAX_DURATION = 0.4;
336
+ var GestureDetector = class {
337
+ onTap = new Signal();
338
+ onLongPress = new Signal();
339
+ onSwipe = new Signal();
340
+ onPinch = new Signal();
341
+ tapMaxDuration;
342
+ tapMaxDistance;
343
+ longPressMinDuration;
344
+ swipeMinDistance;
345
+ swipeMaxDuration;
346
+ // Single-touch tracking
347
+ _startPos = { x: 0, y: 0 };
348
+ _startTime = 0;
349
+ _isTracking = false;
350
+ _longPressFired = false;
351
+ // Pinch tracking
352
+ _pinchStartDistance = 0;
353
+ _pinchTouches = null;
354
+ constructor(config) {
355
+ this.tapMaxDuration = config?.tapMaxDuration ?? DEFAULT_TAP_MAX_DURATION;
356
+ this.tapMaxDistance = config?.tapMaxDistance ?? DEFAULT_TAP_MAX_DISTANCE;
357
+ this.longPressMinDuration = config?.longPressMinDuration ?? DEFAULT_LONG_PRESS_MIN_DURATION;
358
+ this.swipeMinDistance = config?.swipeMinDistance ?? DEFAULT_SWIPE_MIN_DISTANCE;
359
+ this.swipeMaxDuration = config?.swipeMaxDuration ?? DEFAULT_SWIPE_MAX_DURATION;
360
+ }
361
+ /** Feed a pointer-down event. */
362
+ pointerDown(x, y) {
363
+ this._startPos = { x, y };
364
+ this._startTime = 0;
365
+ this._isTracking = true;
366
+ this._longPressFired = false;
367
+ }
368
+ /** Feed a pointer-up event. */
369
+ pointerUp(x, y, elapsedSinceDown) {
370
+ if (!this._isTracking) return;
371
+ this._isTracking = false;
372
+ const dx = x - this._startPos.x;
373
+ const dy = y - this._startPos.y;
374
+ const dist = Math.sqrt(dx * dx + dy * dy);
375
+ const duration = elapsedSinceDown;
376
+ if (dist >= this.swipeMinDistance && duration <= this.swipeMaxDuration) {
377
+ const absDx = Math.abs(dx);
378
+ const absDy = Math.abs(dy);
379
+ let direction;
380
+ if (absDx > absDy) {
381
+ direction = dx > 0 ? "right" : "left";
382
+ } else {
383
+ direction = dy > 0 ? "down" : "up";
384
+ }
385
+ this.onSwipe.emit({ direction, distance: dist, duration });
386
+ return;
387
+ }
388
+ if (dist <= this.tapMaxDistance && duration <= this.tapMaxDuration) {
389
+ this.onTap.emit({ x, y });
390
+ return;
391
+ }
392
+ }
393
+ /**
394
+ * Update gesture tracking. Call each frame with dt.
395
+ * @param dt Delta time in seconds.
396
+ * @param currentX Current pointer X.
397
+ * @param currentY Current pointer Y.
398
+ */
399
+ update(dt, currentX, currentY) {
400
+ if (!this._isTracking) return;
401
+ this._startTime += dt;
402
+ if (!this._longPressFired && this._startTime >= this.longPressMinDuration) {
403
+ const dx = currentX - this._startPos.x;
404
+ const dy = currentY - this._startPos.y;
405
+ const dist = Math.sqrt(dx * dx + dy * dy);
406
+ if (dist <= this.tapMaxDistance) {
407
+ this._longPressFired = true;
408
+ this.onLongPress.emit({ x: currentX, y: currentY });
409
+ }
410
+ }
411
+ }
412
+ /**
413
+ * Feed a two-finger pinch update. Call when two touches are active.
414
+ * @param t1 First touch position.
415
+ * @param t2 Second touch position.
416
+ */
417
+ pinchUpdate(t1, t2) {
418
+ const dx = t2.x - t1.x;
419
+ const dy = t2.y - t1.y;
420
+ const dist = Math.sqrt(dx * dx + dy * dy);
421
+ if (this._pinchTouches === null) {
422
+ this._pinchStartDistance = dist;
423
+ this._pinchTouches = [{ ...t1 }, { ...t2 }];
424
+ return;
425
+ }
426
+ if (this._pinchStartDistance > 0) {
427
+ const scale = dist / this._pinchStartDistance;
428
+ const center = {
429
+ x: (t1.x + t2.x) / 2,
430
+ y: (t1.y + t2.y) / 2
431
+ };
432
+ this.onPinch.emit({ scale, center });
433
+ }
434
+ }
435
+ /** Call when pinch ends (fewer than 2 touches). */
436
+ pinchEnd() {
437
+ this._pinchTouches = null;
438
+ this._pinchStartDistance = 0;
439
+ }
440
+ };
441
+
442
+ // src/virtual-joystick.ts
443
+ var DEFAULT_DEAD_ZONE = 10;
444
+ var DEFAULT_MAX_RADIUS = 50;
445
+ function createJoystickState() {
446
+ return {
447
+ active: false,
448
+ direction: { x: 0, y: 0 },
449
+ magnitude: 0,
450
+ offset: { x: 0, y: 0 }
451
+ };
452
+ }
453
+ function updateJoystick(state, centerX, centerY, touchX, touchY, config) {
454
+ const deadZone = config?.deadZone ?? DEFAULT_DEAD_ZONE;
455
+ const maxRadius = config?.maxRadius ?? DEFAULT_MAX_RADIUS;
456
+ const dx = touchX - centerX;
457
+ const dy = touchY - centerY;
458
+ const dist = Math.sqrt(dx * dx + dy * dy);
459
+ state.active = true;
460
+ if (dist < deadZone) {
461
+ state.direction.x = 0;
462
+ state.direction.y = 0;
463
+ state.magnitude = 0;
464
+ state.offset.x = dx;
465
+ state.offset.y = dy;
466
+ return;
467
+ }
468
+ const clampedDist = Math.min(dist, maxRadius);
469
+ const normalizedDist = (clampedDist - deadZone) / (maxRadius - deadZone);
470
+ const invDist = 1 / dist;
471
+ state.direction.x = dx * invDist;
472
+ state.direction.y = dy * invDist;
473
+ state.magnitude = Math.min(normalizedDist, 1);
474
+ const scale = clampedDist / dist;
475
+ state.offset.x = dx * scale;
476
+ state.offset.y = dy * scale;
477
+ }
478
+ function releaseJoystick(state) {
479
+ state.active = false;
480
+ state.direction.x = 0;
481
+ state.direction.y = 0;
482
+ state.magnitude = 0;
483
+ state.offset.x = 0;
484
+ state.offset.y = 0;
485
+ }
486
+
487
+ // src/virtual-button.ts
488
+ function createButtonState() {
489
+ return {
490
+ isPressed: false,
491
+ justPressed: false,
492
+ justReleased: false
493
+ };
494
+ }
495
+ function resetButtonFrame(state) {
496
+ state.justPressed = false;
497
+ state.justReleased = false;
498
+ }
499
+ function pressButton(state) {
500
+ if (!state.isPressed) {
501
+ state.isPressed = true;
502
+ state.justPressed = true;
503
+ }
504
+ }
505
+ function releaseButton(state) {
506
+ if (state.isPressed) {
507
+ state.isPressed = false;
508
+ state.justReleased = true;
509
+ }
510
+ }
511
+ export {
512
+ GamepadAxis,
513
+ GamepadButton,
514
+ GestureDetector,
515
+ InputManager,
516
+ applyRemap,
517
+ createButtonState,
518
+ createGamepadState,
519
+ createIdentityRemap,
520
+ createJoystickState,
521
+ createPointer,
522
+ createWebGamepadSource,
523
+ getAxis,
524
+ isButtonDown,
525
+ loadRemap,
526
+ pointerDown,
527
+ pointerMove,
528
+ pointerUp,
529
+ pressButton,
530
+ releaseButton,
531
+ releaseJoystick,
532
+ resetButtonFrame,
533
+ resetGamepadFrame,
534
+ resetPointerFrame,
535
+ saveRemap,
536
+ updateGamepadFromSnapshot,
537
+ updateJoystick,
538
+ wasButtonPressed,
539
+ wasButtonReleased
540
+ };
541
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pointer.ts","../src/gamepad.ts","../src/remap.ts","../src/input-manager.ts","../src/gamepad-source.ts","../src/gestures.ts","../src/virtual-joystick.ts","../src/virtual-button.ts"],"sourcesContent":["import type { Vec2 } from '@flare-engine/math';\n\n/** State of a single pointer (primary touch or mouse). */\nexport interface Pointer {\n /** Current position in screen coordinates. */\n position: Vec2;\n /** Position delta since last frame. */\n delta: Vec2;\n /** Whether the pointer is currently held down. */\n isDown: boolean;\n /** True only on the frame the pointer was pressed. */\n justPressed: boolean;\n /** True only on the frame the pointer was released. */\n justReleased: boolean;\n}\n\n/** State of a single touch point in a multitouch scenario. */\nexport interface TouchPoint {\n /** Unique touch identifier. */\n id: number;\n /** Current position in screen coordinates. */\n position: Vec2;\n /** Whether this touch is currently active. */\n isDown: boolean;\n}\n\n/** Create a default pointer state. */\nexport function createPointer(): Pointer {\n return {\n position: { x: 0, y: 0 },\n delta: { x: 0, y: 0 },\n isDown: false,\n justPressed: false,\n justReleased: false,\n };\n}\n\n/**\n * Update pointer state for a new frame.\n * Call this at the start of each frame to clear per-frame flags.\n */\nexport function resetPointerFrame(pointer: Pointer): void {\n pointer.delta.x = 0;\n pointer.delta.y = 0;\n pointer.justPressed = false;\n pointer.justReleased = false;\n}\n\n/** Feed a pointer-down event into the pointer state. */\nexport function pointerDown(pointer: Pointer, x: number, y: number): void {\n const wasDown = pointer.isDown;\n pointer.isDown = true;\n if (!wasDown) {\n pointer.justPressed = true;\n }\n pointer.delta.x = x - pointer.position.x;\n pointer.delta.y = y - pointer.position.y;\n pointer.position.x = x;\n pointer.position.y = y;\n}\n\n/** Feed a pointer-move event into the pointer state. */\nexport function pointerMove(pointer: Pointer, x: number, y: number): void {\n pointer.delta.x = x - pointer.position.x;\n pointer.delta.y = y - pointer.position.y;\n pointer.position.x = x;\n pointer.position.y = y;\n}\n\n/** Feed a pointer-up event into the pointer state. */\nexport function pointerUp(pointer: Pointer, x: number, y: number): void {\n pointer.isDown = false;\n pointer.justReleased = true;\n pointer.delta.x = x - pointer.position.x;\n pointer.delta.y = y - pointer.position.y;\n pointer.position.x = x;\n pointer.position.y = y;\n}\n","/**\n * Gamepad — poll-based state for a single connected controller.\n *\n * Buttons and axes are indexed against the W3C \"Standard Gamepad\" mapping by\n * default. Callers can translate raw indices via `remap.ts` before feeding\n * snapshots into `updateGamepadFromSnapshot`.\n */\n\n/** W3C Standard Gamepad button indices. */\nexport const GamepadButton = {\n A: 0,\n B: 1,\n X: 2,\n Y: 3,\n LB: 4,\n RB: 5,\n LT: 6,\n RT: 7,\n Back: 8,\n Start: 9,\n LS: 10,\n RS: 11,\n DpadUp: 12,\n DpadDown: 13,\n DpadLeft: 14,\n DpadRight: 15,\n Home: 16,\n} as const;\nexport type GamepadButtonId = (typeof GamepadButton)[keyof typeof GamepadButton];\n\n/** W3C Standard Gamepad axes. */\nexport const GamepadAxis = {\n LeftX: 0,\n LeftY: 1,\n RightX: 2,\n RightY: 3,\n} as const;\nexport type GamepadAxisId = (typeof GamepadAxis)[keyof typeof GamepadAxis];\n\nexport interface GamepadButtonState {\n pressed: boolean;\n value: number;\n /** True for exactly one frame after a press. */\n pressedThisFrame: boolean;\n /** True for exactly one frame after a release. */\n releasedThisFrame: boolean;\n}\n\nexport interface GamepadState {\n connected: boolean;\n id: string;\n index: number;\n buttons: GamepadButtonState[];\n axes: number[];\n /** Monotonically increasing timestamp from the source (ms). */\n timestamp: number;\n}\n\n/** Raw snapshot from a `GamepadSource` — values only, no derived state. */\nexport interface GamepadSnapshot {\n id: string;\n index: number;\n buttons: { pressed: boolean; value: number }[];\n axes: number[];\n timestamp: number;\n}\n\nexport function createGamepadState(index = 0, buttonCount = 17, axisCount = 4): GamepadState {\n const buttons: GamepadButtonState[] = new Array(buttonCount);\n for (let i = 0; i < buttonCount; i++) {\n buttons[i] = { pressed: false, value: 0, pressedThisFrame: false, releasedThisFrame: false };\n }\n const axes = new Array<number>(axisCount).fill(0);\n return { connected: false, id: '', index, buttons, axes, timestamp: 0 };\n}\n\nfunction applyDeadZone(value: number, deadZone: number): number {\n const abs = Math.abs(value);\n if (abs <= deadZone) return 0;\n const sign = value < 0 ? -1 : 1;\n // Rescale [deadZone, 1] -> [0, 1] so inputs remain smooth past the dead zone.\n return sign * ((abs - deadZone) / (1 - deadZone));\n}\n\n/**\n * Apply a fresh snapshot to an existing `GamepadState` in place. Derived\n * `pressedThisFrame` / `releasedThisFrame` flags are recomputed from the\n * previous `pressed` value.\n *\n * `deadZone` in [0, 1). Values at or below are treated as 0.\n */\nexport function updateGamepadFromSnapshot(\n state: GamepadState,\n snapshot: GamepadSnapshot | null,\n deadZone = 0,\n): void {\n if (snapshot === null) {\n if (!state.connected && !anyButtonDown(state)) {\n clearFrameFlags(state);\n return;\n }\n // Disconnect: clear edge flags, keep sticky pressed=false for released-edge.\n for (let i = 0; i < state.buttons.length; i++) {\n const b = state.buttons[i];\n if (!b) continue;\n b.pressedThisFrame = false;\n b.releasedThisFrame = b.pressed;\n b.pressed = false;\n b.value = 0;\n }\n for (let i = 0; i < state.axes.length; i++) state.axes[i] = 0;\n state.connected = false;\n state.id = '';\n return;\n }\n\n state.connected = true;\n state.id = snapshot.id;\n state.index = snapshot.index;\n state.timestamp = snapshot.timestamp;\n\n const srcButtons = snapshot.buttons;\n const bLen = Math.min(state.buttons.length, srcButtons.length);\n for (let i = 0; i < bLen; i++) {\n const prev = state.buttons[i];\n const src = srcButtons[i];\n if (!prev || !src) continue;\n const wasPressed = prev.pressed;\n prev.pressed = src.pressed;\n prev.value = src.value;\n prev.pressedThisFrame = src.pressed && !wasPressed;\n prev.releasedThisFrame = !src.pressed && wasPressed;\n }\n // Buttons beyond the snapshot length: clear edges, keep prior pressed false.\n for (let i = bLen; i < state.buttons.length; i++) {\n const b = state.buttons[i];\n if (!b) continue;\n b.pressedThisFrame = false;\n b.releasedThisFrame = b.pressed;\n b.pressed = false;\n b.value = 0;\n }\n\n const aLen = Math.min(state.axes.length, snapshot.axes.length);\n for (let i = 0; i < aLen; i++) {\n state.axes[i] = applyDeadZone(snapshot.axes[i] ?? 0, deadZone);\n }\n for (let i = aLen; i < state.axes.length; i++) state.axes[i] = 0;\n}\n\n/** Reset just the per-frame edge flags. Call when advancing a frame without a fresh snapshot. */\nexport function resetGamepadFrame(state: GamepadState): void {\n clearFrameFlags(state);\n}\n\nfunction clearFrameFlags(state: GamepadState): void {\n for (let i = 0; i < state.buttons.length; i++) {\n const b = state.buttons[i];\n if (!b) continue;\n b.pressedThisFrame = false;\n b.releasedThisFrame = false;\n }\n}\n\nfunction anyButtonDown(state: GamepadState): boolean {\n for (let i = 0; i < state.buttons.length; i++) {\n if (state.buttons[i]?.pressed) return true;\n }\n return false;\n}\n\nexport function isButtonDown(state: GamepadState, button: number): boolean {\n return state.buttons[button]?.pressed ?? false;\n}\n\nexport function wasButtonPressed(state: GamepadState, button: number): boolean {\n return state.buttons[button]?.pressedThisFrame ?? false;\n}\n\nexport function wasButtonReleased(state: GamepadState, button: number): boolean {\n return state.buttons[button]?.releasedThisFrame ?? false;\n}\n\nexport function getAxis(state: GamepadState, axis: number): number {\n return state.axes[axis] ?? 0;\n}\n","import type { GamepadSnapshot } from './gamepad.js';\n\n/**\n * Button / axis remap table.\n *\n * `buttons[logicalId] = physicalIndex` — to poll A, read\n * `snapshot.buttons[table.buttons[GamepadButton.A]]`.\n *\n * `axes` maps logical axis id → `{ index, invert, scale }`. Most games invert\n * Y on some controllers; `scale` supports half-range triggers.\n */\nexport interface AxisMap {\n index: number;\n invert: boolean;\n scale: number;\n}\n\nexport interface RemapTable {\n buttons: number[];\n axes: AxisMap[];\n}\n\nexport function createIdentityRemap(buttonCount = 17, axisCount = 4): RemapTable {\n const buttons = new Array<number>(buttonCount);\n for (let i = 0; i < buttonCount; i++) buttons[i] = i;\n const axes = new Array<AxisMap>(axisCount);\n for (let i = 0; i < axisCount; i++) axes[i] = { index: i, invert: false, scale: 1 };\n return { buttons, axes };\n}\n\n/** Apply a remap table to a raw snapshot, returning a new snapshot with logical ordering. */\nexport function applyRemap(snapshot: GamepadSnapshot, table: RemapTable): GamepadSnapshot {\n const buttons = new Array<{ pressed: boolean; value: number }>(table.buttons.length);\n for (let logical = 0; logical < table.buttons.length; logical++) {\n const physical = table.buttons[logical] ?? logical;\n const src = snapshot.buttons[physical];\n buttons[logical] = { pressed: src?.pressed ?? false, value: src?.value ?? 0 };\n }\n const axes = new Array<number>(table.axes.length);\n for (let logical = 0; logical < table.axes.length; logical++) {\n const map = table.axes[logical] ?? { index: logical, invert: false, scale: 1 };\n const raw = snapshot.axes[map.index] ?? 0;\n axes[logical] = raw * map.scale * (map.invert ? -1 : 1);\n }\n return { id: snapshot.id, index: snapshot.index, timestamp: snapshot.timestamp, buttons, axes };\n}\n\n/**\n * Minimal storage contract — matches `@flare-engine/storage` `StorageAdapter`,\n * but declared locally so this package does not take a hard dep on storage.\n */\nexport interface RemapStore {\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n}\n\n/** Persist a remap table under the given key. Throws on invalid table shape. */\nexport async function saveRemap(store: RemapStore, key: string, table: RemapTable): Promise<void> {\n await store.set(key, JSON.stringify(table));\n}\n\n/** Load a remap table. Returns `null` if absent or malformed. */\nexport async function loadRemap(store: RemapStore, key: string): Promise<RemapTable | null> {\n const raw = await store.get(key);\n if (raw === null) return null;\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!isRemapTable(parsed)) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction isRemapTable(value: unknown): value is RemapTable {\n if (typeof value !== 'object' || value === null) return false;\n const obj = value as { buttons?: unknown; axes?: unknown };\n if (!Array.isArray(obj.buttons) || !Array.isArray(obj.axes)) return false;\n for (const b of obj.buttons) if (typeof b !== 'number') return false;\n for (const a of obj.axes) {\n if (typeof a !== 'object' || a === null) return false;\n const ax = a as { index?: unknown; invert?: unknown; scale?: unknown };\n if (\n typeof ax.index !== 'number' ||\n typeof ax.invert !== 'boolean' ||\n typeof ax.scale !== 'number'\n ) {\n return false;\n }\n }\n return true;\n}\n","import type { GamepadSource } from './gamepad-source.js';\nimport {\n type GamepadState,\n createGamepadState,\n resetGamepadFrame,\n updateGamepadFromSnapshot,\n} from './gamepad.js';\nimport {\n type Pointer,\n type TouchPoint,\n createPointer,\n pointerDown,\n pointerMove,\n pointerUp,\n resetPointerFrame,\n} from './pointer.js';\nimport type { RemapTable } from './remap.js';\nimport { applyRemap } from './remap.js';\n\n/**\n * InputManager provides a polling-based input model.\n *\n * Call `update()` once per frame (before game logic) to snapshot input state.\n * Feed events from the platform (RN touch/gesture handlers) via the on* methods.\n */\nexport class InputManager {\n /** Primary pointer (first touch or mouse). */\n readonly pointer: Pointer = createPointer();\n\n /** All active touch points. */\n readonly touches: TouchPoint[] = [];\n\n private _touchMap = new Map<number, TouchPoint>();\n private _gamepads: GamepadState[] = [];\n private _gamepadDeadZone = 0.15;\n private _gamepadRemaps = new Map<number, RemapTable>();\n\n /** Call once per frame to reset per-frame flags. */\n update(): void {\n resetPointerFrame(this.pointer);\n for (let i = 0; i < this._gamepads.length; i++) {\n const pad = this._gamepads[i];\n if (pad) resetGamepadFrame(pad);\n }\n }\n\n /** Feed a touch/pointer begin event. */\n onPointerDown(id: number, x: number, y: number): void {\n pointerDown(this.pointer, x, y);\n this._addOrUpdateTouch(id, x, y, true);\n }\n\n /** Feed a touch/pointer move event. */\n onPointerMove(id: number, x: number, y: number): void {\n pointerMove(this.pointer, x, y);\n this._addOrUpdateTouch(id, x, y, true);\n }\n\n /** Feed a touch/pointer end event. */\n onPointerUp(id: number, x: number, y: number): void {\n pointerUp(this.pointer, x, y);\n this._removeTouch(id);\n }\n\n /** Access the gamepad state at `index`. Allocates on first access. */\n gamepad(index: number): GamepadState {\n let pad = this._gamepads[index];\n if (!pad) {\n pad = createGamepadState(index);\n this._gamepads[index] = pad;\n }\n return pad;\n }\n\n /** Dead zone in [0, 1). Axes at or below this magnitude read as 0. */\n setGamepadDeadZone(deadZone: number): void {\n this._gamepadDeadZone = Math.max(0, Math.min(0.95, deadZone));\n }\n\n /** Install a remap table for a specific gamepad index. Pass `null` to clear. */\n setGamepadRemap(index: number, table: RemapTable | null): void {\n if (table === null) this._gamepadRemaps.delete(index);\n else this._gamepadRemaps.set(index, table);\n }\n\n /** Poll all gamepads from the given source, applying remap + dead zones. */\n pollGamepads(source: GamepadSource): void {\n const snapshots = source.poll();\n for (let i = 0; i < snapshots.length; i++) {\n const pad = this.gamepad(i);\n const snap = snapshots[i] ?? null;\n const remapped =\n snap !== null\n ? this._gamepadRemaps.get(i)\n ? applyRemap(snap, this._gamepadRemaps.get(i) as RemapTable)\n : snap\n : null;\n updateGamepadFromSnapshot(pad, remapped, this._gamepadDeadZone);\n }\n }\n\n private _addOrUpdateTouch(id: number, x: number, y: number, isDown: boolean): void {\n let touch = this._touchMap.get(id);\n if (!touch) {\n touch = { id, position: { x, y }, isDown };\n this._touchMap.set(id, touch);\n this.touches.push(touch);\n } else {\n touch.position.x = x;\n touch.position.y = y;\n touch.isDown = isDown;\n }\n }\n\n private _removeTouch(id: number): void {\n const touch = this._touchMap.get(id);\n if (!touch) return;\n this._touchMap.delete(id);\n const idx = this.touches.indexOf(touch);\n if (idx !== -1) {\n this.touches.splice(idx, 1);\n }\n }\n}\n","import type { GamepadSnapshot } from './gamepad.js';\n\n/**\n * Platform abstraction for sampling connected gamepads.\n *\n * `poll()` returns one snapshot per slot (typically up to 4). Unconnected\n * slots return `null`. Implementations:\n *\n * - `createWebGamepadSource()` wraps the W3C `navigator.getGamepads` API.\n * - Native implementations (e.g., `react-native-gamepad`) inject their own\n * via the `GamepadSource` interface — the package itself ships no native deps.\n */\nexport interface GamepadSource {\n poll(): (GamepadSnapshot | null)[];\n}\n\ninterface NavigatorLike {\n getGamepads?: () => (WebGamepad | null)[];\n}\n\ninterface WebGamepad {\n id: string;\n index: number;\n connected: boolean;\n timestamp: number;\n buttons: readonly { pressed: boolean; value: number }[];\n axes: readonly number[];\n}\n\n/**\n * Wrap `navigator.getGamepads()` (W3C Gamepad API). If `nav` is not supplied,\n * uses `globalThis.navigator`. Returns an empty array when the API is absent.\n */\nexport function createWebGamepadSource(nav?: NavigatorLike): GamepadSource {\n const source: NavigatorLike | undefined =\n nav ??\n (typeof globalThis === 'undefined'\n ? undefined\n : (globalThis as { navigator?: NavigatorLike }).navigator);\n\n return {\n poll(): (GamepadSnapshot | null)[] {\n if (!source?.getGamepads) return [];\n const pads = source.getGamepads();\n const out: (GamepadSnapshot | null)[] = new Array(pads.length);\n for (let i = 0; i < pads.length; i++) {\n const p = pads[i];\n if (!p || !p.connected) {\n out[i] = null;\n continue;\n }\n out[i] = webGamepadToSnapshot(p);\n }\n return out;\n },\n };\n}\n\nfunction webGamepadToSnapshot(p: WebGamepad): GamepadSnapshot {\n const buttons = new Array<{ pressed: boolean; value: number }>(p.buttons.length);\n for (let i = 0; i < p.buttons.length; i++) {\n const b = p.buttons[i];\n buttons[i] = { pressed: b?.pressed ?? false, value: b?.value ?? 0 };\n }\n const axes = new Array<number>(p.axes.length);\n for (let i = 0; i < p.axes.length; i++) axes[i] = p.axes[i] ?? 0;\n return { id: p.id, index: p.index, timestamp: p.timestamp, buttons, axes };\n}\n","import { Signal } from '@flare-engine/events';\nimport type { Vec2 } from '@flare-engine/math';\n\n/** Swipe direction. */\nexport type SwipeDirection = 'left' | 'right' | 'up' | 'down';\n\n/** Gesture detection configuration. */\nexport interface GestureConfig {\n /** Max time (seconds) for a tap (default 0.3). */\n tapMaxDuration?: number;\n /** Max distance (px) for a tap (default 20). */\n tapMaxDistance?: number;\n /** Min time (seconds) for a long press (default 0.5). */\n longPressMinDuration?: number;\n /** Min distance (px) for a swipe (default 50). */\n swipeMinDistance?: number;\n /** Max time (seconds) for a swipe (default 0.4). */\n swipeMaxDuration?: number;\n}\n\n/** Swipe event data. */\nexport interface SwipeEvent {\n direction: SwipeDirection;\n distance: number;\n duration: number;\n}\n\n/** Pinch event data. */\nexport interface PinchEvent {\n /** Scale factor (>1 = spread, <1 = pinch). */\n scale: number;\n /** Center point between the two touches. */\n center: Vec2;\n}\n\nconst DEFAULT_TAP_MAX_DURATION = 0.3;\nconst DEFAULT_TAP_MAX_DISTANCE = 20;\nconst DEFAULT_LONG_PRESS_MIN_DURATION = 0.5;\nconst DEFAULT_SWIPE_MIN_DISTANCE = 50;\nconst DEFAULT_SWIPE_MAX_DURATION = 0.4;\n\n/**\n * GestureDetector recognizes tap, long press, swipe, and pinch gestures\n * from raw pointer events.\n */\nexport class GestureDetector {\n readonly onTap = new Signal<Vec2>();\n readonly onLongPress = new Signal<Vec2>();\n readonly onSwipe = new Signal<SwipeEvent>();\n readonly onPinch = new Signal<PinchEvent>();\n\n private tapMaxDuration: number;\n private tapMaxDistance: number;\n private longPressMinDuration: number;\n private swipeMinDistance: number;\n private swipeMaxDuration: number;\n\n // Single-touch tracking\n private _startPos: Vec2 = { x: 0, y: 0 };\n private _startTime = 0;\n private _isTracking = false;\n private _longPressFired = false;\n\n // Pinch tracking\n private _pinchStartDistance = 0;\n private _pinchTouches: [Vec2, Vec2] | null = null;\n\n constructor(config?: GestureConfig) {\n this.tapMaxDuration = config?.tapMaxDuration ?? DEFAULT_TAP_MAX_DURATION;\n this.tapMaxDistance = config?.tapMaxDistance ?? DEFAULT_TAP_MAX_DISTANCE;\n this.longPressMinDuration = config?.longPressMinDuration ?? DEFAULT_LONG_PRESS_MIN_DURATION;\n this.swipeMinDistance = config?.swipeMinDistance ?? DEFAULT_SWIPE_MIN_DISTANCE;\n this.swipeMaxDuration = config?.swipeMaxDuration ?? DEFAULT_SWIPE_MAX_DURATION;\n }\n\n /** Feed a pointer-down event. */\n pointerDown(x: number, y: number): void {\n this._startPos = { x, y };\n this._startTime = 0;\n this._isTracking = true;\n this._longPressFired = false;\n }\n\n /** Feed a pointer-up event. */\n pointerUp(x: number, y: number, elapsedSinceDown: number): void {\n if (!this._isTracking) return;\n this._isTracking = false;\n\n const dx = x - this._startPos.x;\n const dy = y - this._startPos.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n const duration = elapsedSinceDown;\n\n // Check swipe first (requires min distance)\n if (dist >= this.swipeMinDistance && duration <= this.swipeMaxDuration) {\n const absDx = Math.abs(dx);\n const absDy = Math.abs(dy);\n let direction: SwipeDirection;\n if (absDx > absDy) {\n direction = dx > 0 ? 'right' : 'left';\n } else {\n direction = dy > 0 ? 'down' : 'up';\n }\n this.onSwipe.emit({ direction, distance: dist, duration });\n return;\n }\n\n // Check tap (short press, small movement)\n if (dist <= this.tapMaxDistance && duration <= this.tapMaxDuration) {\n this.onTap.emit({ x, y });\n return;\n }\n }\n\n /**\n * Update gesture tracking. Call each frame with dt.\n * @param dt Delta time in seconds.\n * @param currentX Current pointer X.\n * @param currentY Current pointer Y.\n */\n update(dt: number, currentX: number, currentY: number): void {\n if (!this._isTracking) return;\n this._startTime += dt;\n\n // Long press detection\n if (!this._longPressFired && this._startTime >= this.longPressMinDuration) {\n const dx = currentX - this._startPos.x;\n const dy = currentY - this._startPos.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n if (dist <= this.tapMaxDistance) {\n this._longPressFired = true;\n this.onLongPress.emit({ x: currentX, y: currentY });\n }\n }\n }\n\n /**\n * Feed a two-finger pinch update. Call when two touches are active.\n * @param t1 First touch position.\n * @param t2 Second touch position.\n */\n pinchUpdate(t1: Vec2, t2: Vec2): void {\n const dx = t2.x - t1.x;\n const dy = t2.y - t1.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (this._pinchTouches === null) {\n // Start pinch\n this._pinchStartDistance = dist;\n this._pinchTouches = [{ ...t1 }, { ...t2 }];\n return;\n }\n\n if (this._pinchStartDistance > 0) {\n const scale = dist / this._pinchStartDistance;\n const center: Vec2 = {\n x: (t1.x + t2.x) / 2,\n y: (t1.y + t2.y) / 2,\n };\n this.onPinch.emit({ scale, center });\n }\n }\n\n /** Call when pinch ends (fewer than 2 touches). */\n pinchEnd(): void {\n this._pinchTouches = null;\n this._pinchStartDistance = 0;\n }\n}\n","import type { Vec2 } from '@flare-engine/math';\n\n/** Virtual joystick configuration. */\nexport interface VirtualJoystickConfig {\n /** Dead zone radius in pixels (default 10). */\n deadZone?: number;\n /** Maximum radius in pixels (default 50). */\n maxRadius?: number;\n}\n\n/** Virtual joystick state — pure data, no UI. */\nexport interface VirtualJoystickState {\n /** Whether the joystick is currently being touched. */\n active: boolean;\n /** Normalized direction vector (-1 to 1 per axis). */\n direction: Vec2;\n /** Magnitude 0-1 (0 = center/dead zone, 1 = max radius). */\n magnitude: number;\n /** Raw offset from center in pixels. */\n offset: Vec2;\n}\n\nconst DEFAULT_DEAD_ZONE = 10;\nconst DEFAULT_MAX_RADIUS = 50;\n\n/** Create a default joystick state. */\nexport function createJoystickState(): VirtualJoystickState {\n return {\n active: false,\n direction: { x: 0, y: 0 },\n magnitude: 0,\n offset: { x: 0, y: 0 },\n };\n}\n\n/**\n * Update joystick state from a touch event.\n *\n * @param state The joystick state to update.\n * @param centerX The joystick center X in screen pixels.\n * @param centerY The joystick center Y in screen pixels.\n * @param touchX The current touch X in screen pixels.\n * @param touchY The current touch Y in screen pixels.\n * @param config Joystick configuration.\n */\nexport function updateJoystick(\n state: VirtualJoystickState,\n centerX: number,\n centerY: number,\n touchX: number,\n touchY: number,\n config?: VirtualJoystickConfig,\n): void {\n const deadZone = config?.deadZone ?? DEFAULT_DEAD_ZONE;\n const maxRadius = config?.maxRadius ?? DEFAULT_MAX_RADIUS;\n\n const dx = touchX - centerX;\n const dy = touchY - centerY;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n state.active = true;\n\n if (dist < deadZone) {\n state.direction.x = 0;\n state.direction.y = 0;\n state.magnitude = 0;\n state.offset.x = dx;\n state.offset.y = dy;\n return;\n }\n\n // Clamp to max radius\n const clampedDist = Math.min(dist, maxRadius);\n const normalizedDist = (clampedDist - deadZone) / (maxRadius - deadZone);\n\n // Direction is always the full normalized direction\n const invDist = 1 / dist;\n state.direction.x = dx * invDist;\n state.direction.y = dy * invDist;\n state.magnitude = Math.min(normalizedDist, 1);\n\n // Offset clamped to max radius\n const scale = clampedDist / dist;\n state.offset.x = dx * scale;\n state.offset.y = dy * scale;\n}\n\n/** Reset joystick state when touch is released. */\nexport function releaseJoystick(state: VirtualJoystickState): void {\n state.active = false;\n state.direction.x = 0;\n state.direction.y = 0;\n state.magnitude = 0;\n state.offset.x = 0;\n state.offset.y = 0;\n}\n","/** Virtual button state — pure data, no UI. */\nexport interface VirtualButtonState {\n /** Whether the button is currently pressed. */\n isPressed: boolean;\n /** Whether the button was just pressed this frame. */\n justPressed: boolean;\n /** Whether the button was just released this frame. */\n justReleased: boolean;\n}\n\n/** Create a default button state. */\nexport function createButtonState(): VirtualButtonState {\n return {\n isPressed: false,\n justPressed: false,\n justReleased: false,\n };\n}\n\n/** Call at the start of each frame to reset per-frame flags. */\nexport function resetButtonFrame(state: VirtualButtonState): void {\n state.justPressed = false;\n state.justReleased = false;\n}\n\n/** Call when the button is pressed. */\nexport function pressButton(state: VirtualButtonState): void {\n if (!state.isPressed) {\n state.isPressed = true;\n state.justPressed = true;\n }\n}\n\n/** Call when the button is released. */\nexport function releaseButton(state: VirtualButtonState): void {\n if (state.isPressed) {\n state.isPressed = false;\n state.justReleased = true;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@flare-engine/input",
3
+ "version": "0.1.0",
4
+ "description": "InputManager, VirtualJoystick, gesture handling",
5
+ "keywords": ["flare-engine", "input", "joystick", "gestures"],
6
+ "license": "MIT",
7
+ "author": "Grzegorz Otto",
8
+ "homepage": "https://github.com/grzott/flare-engine#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/grzott/flare-engine/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/grzott/flare-engine.git",
15
+ "directory": "packages/input"
16
+ },
17
+ "main": "dist/index.js",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.mjs",
24
+ "require": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": ["dist", "README.md", "LICENSE"],
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "test": "bun test",
31
+ "lint": "biome check src/",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "dependencies": {
35
+ "@flare-engine/core": "^0.1.0",
36
+ "@flare-engine/events": "^0.1.0",
37
+ "@flare-engine/math": "^0.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react-native-gesture-handler": ">=2.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "restricted",
44
+ "registry": "https://registry.npmjs.org/"
45
+ },
46
+ "sideEffects": false
47
+ }