@flare-engine/input 0.2.1 → 0.3.1
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/LICENSE +201 -21
- package/NOTICE +2 -0
- package/README.md +33 -5
- package/dist/index.d.mts +310 -230
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +310 -230
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +729 -553
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +695 -499
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -11
package/dist/index.mjs
CHANGED
|
@@ -1,541 +1,737 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
1
|
+
import { Signal } from "@flare-engine/events";
|
|
2
|
+
//#region src/gamepad.ts
|
|
3
|
+
/**
|
|
4
|
+
* Gamepad — poll-based state for a single connected controller.
|
|
5
|
+
*
|
|
6
|
+
* Buttons and axes are indexed against the W3C "Standard Gamepad" mapping by
|
|
7
|
+
* default. Callers can translate raw indices via `remap.ts` before feeding
|
|
8
|
+
* snapshots into `updateGamepadFromSnapshot`.
|
|
9
|
+
*/
|
|
10
|
+
/** W3C Standard Gamepad button indices. */
|
|
11
|
+
const GamepadButton = {
|
|
12
|
+
A: 0,
|
|
13
|
+
B: 1,
|
|
14
|
+
X: 2,
|
|
15
|
+
Y: 3,
|
|
16
|
+
LB: 4,
|
|
17
|
+
RB: 5,
|
|
18
|
+
LT: 6,
|
|
19
|
+
RT: 7,
|
|
20
|
+
Back: 8,
|
|
21
|
+
Start: 9,
|
|
22
|
+
LS: 10,
|
|
23
|
+
RS: 11,
|
|
24
|
+
DpadUp: 12,
|
|
25
|
+
DpadDown: 13,
|
|
26
|
+
DpadLeft: 14,
|
|
27
|
+
DpadRight: 15,
|
|
28
|
+
Home: 16
|
|
62
29
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
30
|
+
/** W3C Standard Gamepad axes. */
|
|
31
|
+
const GamepadAxis = {
|
|
32
|
+
LeftX: 0,
|
|
33
|
+
LeftY: 1,
|
|
34
|
+
RightX: 2,
|
|
35
|
+
RightY: 3
|
|
68
36
|
};
|
|
69
37
|
function createGamepadState(index = 0, buttonCount = 17, axisCount = 4) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
38
|
+
const buttons = new Array(buttonCount);
|
|
39
|
+
for (let i = 0; i < buttonCount; i++) buttons[i] = {
|
|
40
|
+
pressed: false,
|
|
41
|
+
value: 0,
|
|
42
|
+
pressedThisFrame: false,
|
|
43
|
+
releasedThisFrame: false
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
connected: false,
|
|
47
|
+
id: "",
|
|
48
|
+
index,
|
|
49
|
+
buttons,
|
|
50
|
+
axes: new Array(axisCount).fill(0),
|
|
51
|
+
timestamp: 0
|
|
52
|
+
};
|
|
76
53
|
}
|
|
77
54
|
function applyDeadZone(value, deadZone) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return sign * ((abs - deadZone) / (1 - deadZone));
|
|
55
|
+
const abs = Math.abs(value);
|
|
56
|
+
if (abs <= deadZone) return 0;
|
|
57
|
+
return (value < 0 ? -1 : 1) * ((abs - deadZone) / (1 - deadZone));
|
|
82
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Apply a fresh snapshot to an existing `GamepadState` in place. Derived
|
|
61
|
+
* `pressedThisFrame` / `releasedThisFrame` flags are recomputed from the
|
|
62
|
+
* previous `pressed` value.
|
|
63
|
+
*
|
|
64
|
+
* `deadZone` in [0, 1). Values at or below are treated as 0.
|
|
65
|
+
*/
|
|
83
66
|
function updateGamepadFromSnapshot(state, snapshot, deadZone = 0) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
for (let i = aLen; i < state.axes.length; i++) state.axes[i] = 0;
|
|
67
|
+
if (snapshot === null) {
|
|
68
|
+
if (!state.connected && !anyButtonDown(state)) {
|
|
69
|
+
clearFrameFlags(state);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (let i = 0; i < state.buttons.length; i++) {
|
|
73
|
+
const b = state.buttons[i];
|
|
74
|
+
if (!b) continue;
|
|
75
|
+
b.pressedThisFrame = false;
|
|
76
|
+
b.releasedThisFrame = b.pressed;
|
|
77
|
+
b.pressed = false;
|
|
78
|
+
b.value = 0;
|
|
79
|
+
}
|
|
80
|
+
for (let i = 0; i < state.axes.length; i++) state.axes[i] = 0;
|
|
81
|
+
state.connected = false;
|
|
82
|
+
state.id = "";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
state.connected = true;
|
|
86
|
+
state.id = snapshot.id;
|
|
87
|
+
state.index = snapshot.index;
|
|
88
|
+
state.timestamp = snapshot.timestamp;
|
|
89
|
+
const srcButtons = snapshot.buttons;
|
|
90
|
+
const bLen = Math.min(state.buttons.length, srcButtons.length);
|
|
91
|
+
for (let i = 0; i < bLen; i++) {
|
|
92
|
+
const prev = state.buttons[i];
|
|
93
|
+
const src = srcButtons[i];
|
|
94
|
+
if (!prev || !src) continue;
|
|
95
|
+
const wasPressed = prev.pressed;
|
|
96
|
+
prev.pressed = src.pressed;
|
|
97
|
+
prev.value = src.value;
|
|
98
|
+
prev.pressedThisFrame = src.pressed && !wasPressed;
|
|
99
|
+
prev.releasedThisFrame = !src.pressed && wasPressed;
|
|
100
|
+
}
|
|
101
|
+
for (let i = bLen; i < state.buttons.length; i++) {
|
|
102
|
+
const b = state.buttons[i];
|
|
103
|
+
if (!b) continue;
|
|
104
|
+
b.pressedThisFrame = false;
|
|
105
|
+
b.releasedThisFrame = b.pressed;
|
|
106
|
+
b.pressed = false;
|
|
107
|
+
b.value = 0;
|
|
108
|
+
}
|
|
109
|
+
const aLen = Math.min(state.axes.length, snapshot.axes.length);
|
|
110
|
+
for (let i = 0; i < aLen; i++) state.axes[i] = applyDeadZone(snapshot.axes[i] ?? 0, deadZone);
|
|
111
|
+
for (let i = aLen; i < state.axes.length; i++) state.axes[i] = 0;
|
|
131
112
|
}
|
|
113
|
+
/** Reset just the per-frame edge flags. Call when advancing a frame without a fresh snapshot. */
|
|
132
114
|
function resetGamepadFrame(state) {
|
|
133
|
-
|
|
115
|
+
clearFrameFlags(state);
|
|
134
116
|
}
|
|
135
117
|
function clearFrameFlags(state) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
118
|
+
for (let i = 0; i < state.buttons.length; i++) {
|
|
119
|
+
const b = state.buttons[i];
|
|
120
|
+
if (!b) continue;
|
|
121
|
+
b.pressedThisFrame = false;
|
|
122
|
+
b.releasedThisFrame = false;
|
|
123
|
+
}
|
|
142
124
|
}
|
|
143
125
|
function anyButtonDown(state) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
126
|
+
for (let i = 0; i < state.buttons.length; i++) if (state.buttons[i]?.pressed) return true;
|
|
127
|
+
return false;
|
|
148
128
|
}
|
|
149
129
|
function isButtonDown(state, button) {
|
|
150
|
-
|
|
130
|
+
return state.buttons[button]?.pressed ?? false;
|
|
151
131
|
}
|
|
152
132
|
function wasButtonPressed(state, button) {
|
|
153
|
-
|
|
133
|
+
return state.buttons[button]?.pressedThisFrame ?? false;
|
|
154
134
|
}
|
|
155
135
|
function wasButtonReleased(state, button) {
|
|
156
|
-
|
|
136
|
+
return state.buttons[button]?.releasedThisFrame ?? false;
|
|
157
137
|
}
|
|
158
138
|
function getAxis(state, axis) {
|
|
159
|
-
|
|
139
|
+
return state.axes[axis] ?? 0;
|
|
160
140
|
}
|
|
161
|
-
|
|
162
|
-
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/gamepad-source.ts
|
|
143
|
+
/**
|
|
144
|
+
* Wrap `navigator.getGamepads()` (W3C Gamepad API). If `nav` is not supplied,
|
|
145
|
+
* uses `globalThis.navigator`. Returns an empty array when the API is absent.
|
|
146
|
+
*/
|
|
147
|
+
function createWebGamepadSource(nav) {
|
|
148
|
+
const source = nav ?? (typeof globalThis === "undefined" ? void 0 : globalThis.navigator);
|
|
149
|
+
return { poll() {
|
|
150
|
+
if (!source?.getGamepads) return [];
|
|
151
|
+
const pads = source.getGamepads();
|
|
152
|
+
const out = new Array(pads.length);
|
|
153
|
+
for (let i = 0; i < pads.length; i++) {
|
|
154
|
+
const p = pads[i];
|
|
155
|
+
if (!p?.connected) {
|
|
156
|
+
out[i] = null;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
out[i] = webGamepadToSnapshot(p);
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
} };
|
|
163
|
+
}
|
|
164
|
+
function webGamepadToSnapshot(p) {
|
|
165
|
+
const buttons = new Array(p.buttons.length);
|
|
166
|
+
for (let i = 0; i < p.buttons.length; i++) {
|
|
167
|
+
const b = p.buttons[i];
|
|
168
|
+
buttons[i] = {
|
|
169
|
+
pressed: b?.pressed ?? false,
|
|
170
|
+
value: b?.value ?? 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const axes = new Array(p.axes.length);
|
|
174
|
+
for (let i = 0; i < p.axes.length; i++) axes[i] = p.axes[i] ?? 0;
|
|
175
|
+
return {
|
|
176
|
+
id: p.id,
|
|
177
|
+
index: p.index,
|
|
178
|
+
timestamp: p.timestamp,
|
|
179
|
+
buttons,
|
|
180
|
+
axes
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/gestures.ts
|
|
185
|
+
const DEFAULT_TAP_MAX_DURATION = .3;
|
|
186
|
+
const DEFAULT_TAP_MAX_DISTANCE = 20;
|
|
187
|
+
const DEFAULT_LONG_PRESS_MIN_DURATION = .5;
|
|
188
|
+
const DEFAULT_SWIPE_MIN_DISTANCE = 50;
|
|
189
|
+
const DEFAULT_SWIPE_MAX_DURATION = .4;
|
|
190
|
+
/**
|
|
191
|
+
* GestureDetector recognizes tap, long press, swipe, and pinch gestures
|
|
192
|
+
* from raw pointer events.
|
|
193
|
+
*/
|
|
194
|
+
var GestureDetector = class {
|
|
195
|
+
onTap = new Signal();
|
|
196
|
+
onLongPress = new Signal();
|
|
197
|
+
onSwipe = new Signal();
|
|
198
|
+
onPinch = new Signal();
|
|
199
|
+
tapMaxDuration;
|
|
200
|
+
tapMaxDistance;
|
|
201
|
+
longPressMinDuration;
|
|
202
|
+
swipeMinDistance;
|
|
203
|
+
swipeMaxDuration;
|
|
204
|
+
_startPos = {
|
|
205
|
+
x: 0,
|
|
206
|
+
y: 0
|
|
207
|
+
};
|
|
208
|
+
_startTime = 0;
|
|
209
|
+
_isTracking = false;
|
|
210
|
+
_longPressFired = false;
|
|
211
|
+
_pinchStartDistance = 0;
|
|
212
|
+
_pinchTouches = null;
|
|
213
|
+
constructor(config) {
|
|
214
|
+
this.tapMaxDuration = config?.tapMaxDuration ?? DEFAULT_TAP_MAX_DURATION;
|
|
215
|
+
this.tapMaxDistance = config?.tapMaxDistance ?? DEFAULT_TAP_MAX_DISTANCE;
|
|
216
|
+
this.longPressMinDuration = config?.longPressMinDuration ?? DEFAULT_LONG_PRESS_MIN_DURATION;
|
|
217
|
+
this.swipeMinDistance = config?.swipeMinDistance ?? DEFAULT_SWIPE_MIN_DISTANCE;
|
|
218
|
+
this.swipeMaxDuration = config?.swipeMaxDuration ?? DEFAULT_SWIPE_MAX_DURATION;
|
|
219
|
+
}
|
|
220
|
+
/** Feed a pointer-down event. */
|
|
221
|
+
pointerDown(x, y) {
|
|
222
|
+
this._startPos = {
|
|
223
|
+
x,
|
|
224
|
+
y
|
|
225
|
+
};
|
|
226
|
+
this._startTime = 0;
|
|
227
|
+
this._isTracking = true;
|
|
228
|
+
this._longPressFired = false;
|
|
229
|
+
}
|
|
230
|
+
/** Feed a pointer-up event. */
|
|
231
|
+
pointerUp(x, y, elapsedSinceDown) {
|
|
232
|
+
if (!this._isTracking) return;
|
|
233
|
+
this._isTracking = false;
|
|
234
|
+
const dx = x - this._startPos.x;
|
|
235
|
+
const dy = y - this._startPos.y;
|
|
236
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
237
|
+
const duration = elapsedSinceDown;
|
|
238
|
+
if (dist >= this.swipeMinDistance && duration <= this.swipeMaxDuration) {
|
|
239
|
+
const absDx = Math.abs(dx);
|
|
240
|
+
const absDy = Math.abs(dy);
|
|
241
|
+
let direction;
|
|
242
|
+
if (absDx > absDy) direction = dx > 0 ? "right" : "left";
|
|
243
|
+
else direction = dy > 0 ? "down" : "up";
|
|
244
|
+
this.onSwipe.emit({
|
|
245
|
+
direction,
|
|
246
|
+
distance: dist,
|
|
247
|
+
duration
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (dist <= this.tapMaxDistance && duration <= this.tapMaxDuration) {
|
|
252
|
+
this.onTap.emit({
|
|
253
|
+
x,
|
|
254
|
+
y
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Update gesture tracking. Call each frame with dt.
|
|
261
|
+
* @param dt Delta time in seconds.
|
|
262
|
+
* @param currentX Current pointer X.
|
|
263
|
+
* @param currentY Current pointer Y.
|
|
264
|
+
*/
|
|
265
|
+
update(dt, currentX, currentY) {
|
|
266
|
+
if (!this._isTracking) return;
|
|
267
|
+
this._startTime += dt;
|
|
268
|
+
if (!this._longPressFired && this._startTime >= this.longPressMinDuration) {
|
|
269
|
+
const dx = currentX - this._startPos.x;
|
|
270
|
+
const dy = currentY - this._startPos.y;
|
|
271
|
+
if (Math.sqrt(dx * dx + dy * dy) <= this.tapMaxDistance) {
|
|
272
|
+
this._longPressFired = true;
|
|
273
|
+
this.onLongPress.emit({
|
|
274
|
+
x: currentX,
|
|
275
|
+
y: currentY
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Feed a two-finger pinch update. Call when two touches are active.
|
|
282
|
+
* @param t1 First touch position.
|
|
283
|
+
* @param t2 Second touch position.
|
|
284
|
+
*/
|
|
285
|
+
pinchUpdate(t1, t2) {
|
|
286
|
+
const dx = t2.x - t1.x;
|
|
287
|
+
const dy = t2.y - t1.y;
|
|
288
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
289
|
+
if (this._pinchTouches === null) {
|
|
290
|
+
this._pinchStartDistance = dist;
|
|
291
|
+
this._pinchTouches = [{ ...t1 }, { ...t2 }];
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (this._pinchStartDistance > 0) {
|
|
295
|
+
const scale = dist / this._pinchStartDistance;
|
|
296
|
+
const center = {
|
|
297
|
+
x: (t1.x + t2.x) / 2,
|
|
298
|
+
y: (t1.y + t2.y) / 2
|
|
299
|
+
};
|
|
300
|
+
this.onPinch.emit({
|
|
301
|
+
scale,
|
|
302
|
+
center
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Call when pinch ends (fewer than 2 touches). */
|
|
307
|
+
pinchEnd() {
|
|
308
|
+
this._pinchTouches = null;
|
|
309
|
+
this._pinchStartDistance = 0;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/virtual-button.ts
|
|
314
|
+
/** Create a default button state. */
|
|
315
|
+
function createButtonState() {
|
|
316
|
+
return {
|
|
317
|
+
isPressed: false,
|
|
318
|
+
justPressed: false,
|
|
319
|
+
justReleased: false
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/** Call at the start of each frame to reset per-frame flags. */
|
|
323
|
+
function resetButtonFrame(state) {
|
|
324
|
+
state.justPressed = false;
|
|
325
|
+
state.justReleased = false;
|
|
326
|
+
}
|
|
327
|
+
/** Call when the button is pressed. */
|
|
328
|
+
function pressButton(state) {
|
|
329
|
+
if (!state.isPressed) {
|
|
330
|
+
state.isPressed = true;
|
|
331
|
+
state.justPressed = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Call when the button is released. */
|
|
335
|
+
function releaseButton(state) {
|
|
336
|
+
if (state.isPressed) {
|
|
337
|
+
state.isPressed = false;
|
|
338
|
+
state.justReleased = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/keyboard.ts
|
|
343
|
+
/** Create a fresh keyboard state. */
|
|
344
|
+
function createKeyboard() {
|
|
345
|
+
const states = [];
|
|
346
|
+
const codeIndex = /* @__PURE__ */ new Map();
|
|
347
|
+
return {
|
|
348
|
+
isDown(code) {
|
|
349
|
+
const i = codeIndex.get(code);
|
|
350
|
+
if (i === void 0) return false;
|
|
351
|
+
return states[i]?.isPressed ?? false;
|
|
352
|
+
},
|
|
353
|
+
wasPressed(code) {
|
|
354
|
+
const i = codeIndex.get(code);
|
|
355
|
+
if (i === void 0) return false;
|
|
356
|
+
return states[i]?.justPressed ?? false;
|
|
357
|
+
},
|
|
358
|
+
wasReleased(code) {
|
|
359
|
+
const i = codeIndex.get(code);
|
|
360
|
+
if (i === void 0) return false;
|
|
361
|
+
return states[i]?.justReleased ?? false;
|
|
362
|
+
},
|
|
363
|
+
keyDown(code) {
|
|
364
|
+
let i = codeIndex.get(code);
|
|
365
|
+
if (i === void 0) {
|
|
366
|
+
i = states.length;
|
|
367
|
+
states.push(createButtonState());
|
|
368
|
+
codeIndex.set(code, i);
|
|
369
|
+
}
|
|
370
|
+
pressButton(states[i]);
|
|
371
|
+
},
|
|
372
|
+
keyUp(code) {
|
|
373
|
+
const i = codeIndex.get(code);
|
|
374
|
+
if (i === void 0) return;
|
|
375
|
+
releaseButton(states[i]);
|
|
376
|
+
},
|
|
377
|
+
reset() {
|
|
378
|
+
for (let i = 0; i < states.length; i++) resetButtonFrame(states[i]);
|
|
379
|
+
},
|
|
380
|
+
clear() {
|
|
381
|
+
for (let i = 0; i < states.length; i++) releaseButton(states[i]);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** Free-function parity with `resetPointerFrame`. Delegates to `kb.reset()`. */
|
|
386
|
+
function resetKeyboardFrame(kb) {
|
|
387
|
+
kb.reset();
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Web turnkey adapter: registers `keydown`, `keyup`, and `blur` listeners on
|
|
391
|
+
* `target`. `blur` calls `kb.clear()` (stuck-key guard). Returns a detach fn.
|
|
392
|
+
*
|
|
393
|
+
* `target` is structural `{ addEventListener, removeEventListener }` — no DOM
|
|
394
|
+
* types dep. On web, pass `window`; for mobile/RN, feed `keyDown`/`keyUp`
|
|
395
|
+
* directly from a hardware-keyboard native module or an on-screen source.
|
|
396
|
+
*
|
|
397
|
+
* Does NOT call `preventDefault` — bare use lets Space/arrows scroll and Tab
|
|
398
|
+
* move DOM focus. For game-mode input, attach your own listener that calls
|
|
399
|
+
* `preventDefault` for game keys and forwards `kb.keyDown(ev.code)`.
|
|
400
|
+
*/
|
|
401
|
+
function attachKeyboardListeners(kb, target) {
|
|
402
|
+
const onKeyDown = (ev) => {
|
|
403
|
+
kb.keyDown(ev.code);
|
|
404
|
+
};
|
|
405
|
+
const onKeyUp = (ev) => {
|
|
406
|
+
kb.keyUp(ev.code);
|
|
407
|
+
};
|
|
408
|
+
const onBlur = (_ev) => {
|
|
409
|
+
kb.clear();
|
|
410
|
+
};
|
|
411
|
+
target.addEventListener("keydown", onKeyDown);
|
|
412
|
+
target.addEventListener("keyup", onKeyUp);
|
|
413
|
+
target.addEventListener("blur", onBlur);
|
|
414
|
+
return () => {
|
|
415
|
+
target.removeEventListener("keydown", onKeyDown);
|
|
416
|
+
target.removeEventListener("keyup", onKeyUp);
|
|
417
|
+
target.removeEventListener("blur", onBlur);
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/** True if any bound code for `action` is currently held. */
|
|
421
|
+
function isActionDown(kb, bindings, action) {
|
|
422
|
+
const codes = bindings[action];
|
|
423
|
+
if (!codes) return false;
|
|
424
|
+
for (let i = 0; i < codes.length; i++) if (kb.isDown(codes[i])) return true;
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
/** True if any bound code for `action` was pressed this frame. */
|
|
428
|
+
function wasActionPressed(kb, bindings, action) {
|
|
429
|
+
const codes = bindings[action];
|
|
430
|
+
if (!codes) return false;
|
|
431
|
+
for (let i = 0; i < codes.length; i++) if (kb.wasPressed(codes[i])) return true;
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* True if any bound code for `action` was released this frame AND no bound
|
|
436
|
+
* code is still held. Release fires only when the last held bound key lifts.
|
|
437
|
+
*/
|
|
438
|
+
function wasActionReleased(kb, bindings, action) {
|
|
439
|
+
const codes = bindings[action];
|
|
440
|
+
if (!codes) return false;
|
|
441
|
+
let anyReleased = false;
|
|
442
|
+
for (let i = 0; i < codes.length; i++) {
|
|
443
|
+
if (kb.isDown(codes[i])) return false;
|
|
444
|
+
if (kb.wasReleased(codes[i])) anyReleased = true;
|
|
445
|
+
}
|
|
446
|
+
return anyReleased;
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/pointer.ts
|
|
450
|
+
/** Create a default pointer state. */
|
|
451
|
+
function createPointer() {
|
|
452
|
+
return {
|
|
453
|
+
position: {
|
|
454
|
+
x: 0,
|
|
455
|
+
y: 0
|
|
456
|
+
},
|
|
457
|
+
delta: {
|
|
458
|
+
x: 0,
|
|
459
|
+
y: 0
|
|
460
|
+
},
|
|
461
|
+
isDown: false,
|
|
462
|
+
justPressed: false,
|
|
463
|
+
justReleased: false
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Update pointer state for a new frame.
|
|
468
|
+
* Call this at the start of each frame to clear per-frame flags.
|
|
469
|
+
*/
|
|
470
|
+
function resetPointerFrame(pointer) {
|
|
471
|
+
pointer.delta.x = 0;
|
|
472
|
+
pointer.delta.y = 0;
|
|
473
|
+
pointer.justPressed = false;
|
|
474
|
+
pointer.justReleased = false;
|
|
475
|
+
}
|
|
476
|
+
/** Feed a pointer-down event into the pointer state. */
|
|
477
|
+
function pointerDown(pointer, x, y) {
|
|
478
|
+
const wasDown = pointer.isDown;
|
|
479
|
+
pointer.isDown = true;
|
|
480
|
+
if (!wasDown) pointer.justPressed = true;
|
|
481
|
+
pointer.delta.x += x - pointer.position.x;
|
|
482
|
+
pointer.delta.y += y - pointer.position.y;
|
|
483
|
+
pointer.position.x = x;
|
|
484
|
+
pointer.position.y = y;
|
|
485
|
+
}
|
|
486
|
+
/** Feed a pointer-move event into the pointer state. */
|
|
487
|
+
function pointerMove(pointer, x, y) {
|
|
488
|
+
pointer.delta.x += x - pointer.position.x;
|
|
489
|
+
pointer.delta.y += y - pointer.position.y;
|
|
490
|
+
pointer.position.x = x;
|
|
491
|
+
pointer.position.y = y;
|
|
492
|
+
}
|
|
493
|
+
/** Feed a pointer-up event into the pointer state. */
|
|
494
|
+
function pointerUp(pointer, x, y) {
|
|
495
|
+
pointer.isDown = false;
|
|
496
|
+
pointer.justReleased = true;
|
|
497
|
+
pointer.delta.x += x - pointer.position.x;
|
|
498
|
+
pointer.delta.y += y - pointer.position.y;
|
|
499
|
+
pointer.position.x = x;
|
|
500
|
+
pointer.position.y = y;
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/remap.ts
|
|
163
504
|
function createIdentityRemap(buttonCount = 17, axisCount = 4) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
505
|
+
const buttons = new Array(buttonCount);
|
|
506
|
+
for (let i = 0; i < buttonCount; i++) buttons[i] = i;
|
|
507
|
+
const axes = new Array(axisCount);
|
|
508
|
+
for (let i = 0; i < axisCount; i++) axes[i] = {
|
|
509
|
+
index: i,
|
|
510
|
+
invert: false,
|
|
511
|
+
scale: 1
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
buttons,
|
|
515
|
+
axes
|
|
516
|
+
};
|
|
169
517
|
}
|
|
518
|
+
/** Apply a remap table to a raw snapshot, returning a new snapshot with logical ordering. */
|
|
170
519
|
function applyRemap(snapshot, table) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
520
|
+
const buttons = new Array(table.buttons.length);
|
|
521
|
+
for (let logical = 0; logical < table.buttons.length; logical++) {
|
|
522
|
+
const physical = table.buttons[logical] ?? logical;
|
|
523
|
+
const src = snapshot.buttons[physical];
|
|
524
|
+
buttons[logical] = {
|
|
525
|
+
pressed: src?.pressed ?? false,
|
|
526
|
+
value: src?.value ?? 0
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const axes = new Array(table.axes.length);
|
|
530
|
+
for (let logical = 0; logical < table.axes.length; logical++) {
|
|
531
|
+
const map = table.axes[logical] ?? {
|
|
532
|
+
index: logical,
|
|
533
|
+
invert: false,
|
|
534
|
+
scale: 1
|
|
535
|
+
};
|
|
536
|
+
axes[logical] = (snapshot.axes[map.index] ?? 0) * map.scale * (map.invert ? -1 : 1);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
id: snapshot.id,
|
|
540
|
+
index: snapshot.index,
|
|
541
|
+
timestamp: snapshot.timestamp,
|
|
542
|
+
buttons,
|
|
543
|
+
axes
|
|
544
|
+
};
|
|
184
545
|
}
|
|
546
|
+
/** Persist a remap table under the given key. Throws on invalid table shape. */
|
|
185
547
|
async function saveRemap(store, key, table) {
|
|
186
|
-
|
|
548
|
+
await store.set(key, JSON.stringify(table));
|
|
187
549
|
}
|
|
550
|
+
/** Load a remap table. Returns `null` if absent or malformed. */
|
|
188
551
|
async function loadRemap(store, key) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
552
|
+
const raw = await store.get(key);
|
|
553
|
+
if (raw === null) return null;
|
|
554
|
+
try {
|
|
555
|
+
const parsed = JSON.parse(raw);
|
|
556
|
+
if (!isRemapTable(parsed)) return null;
|
|
557
|
+
return parsed;
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
198
561
|
}
|
|
199
562
|
function isRemapTable(value) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
return true;
|
|
563
|
+
if (typeof value !== "object" || value === null) return false;
|
|
564
|
+
const obj = value;
|
|
565
|
+
if (!Array.isArray(obj.buttons) || !Array.isArray(obj.axes)) return false;
|
|
566
|
+
for (const b of obj.buttons) if (typeof b !== "number") return false;
|
|
567
|
+
for (const a of obj.axes) {
|
|
568
|
+
if (typeof a !== "object" || a === null) return false;
|
|
569
|
+
const ax = a;
|
|
570
|
+
if (typeof ax.index !== "number" || typeof ax.invert !== "boolean" || typeof ax.scale !== "number") return false;
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
212
573
|
}
|
|
213
|
-
|
|
214
|
-
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/input-manager.ts
|
|
576
|
+
/**
|
|
577
|
+
* InputManager provides a polling-based input model.
|
|
578
|
+
*
|
|
579
|
+
* Call `update()` once per frame (before game logic) to snapshot input state.
|
|
580
|
+
* Feed events from the platform (RN touch/gesture handlers) via the on* methods.
|
|
581
|
+
*/
|
|
215
582
|
var InputManager = class {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
583
|
+
/** Primary pointer (first touch or mouse). */
|
|
584
|
+
pointer = createPointer();
|
|
585
|
+
/** Buffered keyboard state. Feed via `attachKeyboardListeners` (web) or `keyboard.keyDown`/`keyUp` (mobile/RN). */
|
|
586
|
+
keyboard = createKeyboard();
|
|
587
|
+
/** All active touch points. */
|
|
588
|
+
touches = [];
|
|
589
|
+
_touchMap = /* @__PURE__ */ new Map();
|
|
590
|
+
_gamepads = [];
|
|
591
|
+
_gamepadDeadZone = .15;
|
|
592
|
+
_gamepadRemaps = /* @__PURE__ */ new Map();
|
|
593
|
+
/** Call once per frame to reset per-frame flags. */
|
|
594
|
+
update() {
|
|
595
|
+
resetPointerFrame(this.pointer);
|
|
596
|
+
resetKeyboardFrame(this.keyboard);
|
|
597
|
+
for (let i = 0; i < this._gamepads.length; i++) {
|
|
598
|
+
const pad = this._gamepads[i];
|
|
599
|
+
if (pad) resetGamepadFrame(pad);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Feed a touch/pointer begin event. */
|
|
603
|
+
onPointerDown(id, x, y) {
|
|
604
|
+
pointerDown(this.pointer, x, y);
|
|
605
|
+
this._addOrUpdateTouch(id, x, y, true);
|
|
606
|
+
}
|
|
607
|
+
/** Feed a touch/pointer move event. */
|
|
608
|
+
onPointerMove(id, x, y) {
|
|
609
|
+
pointerMove(this.pointer, x, y);
|
|
610
|
+
this._addOrUpdateTouch(id, x, y, true);
|
|
611
|
+
}
|
|
612
|
+
/** Feed a touch/pointer end event. */
|
|
613
|
+
onPointerUp(id, x, y) {
|
|
614
|
+
pointerUp(this.pointer, x, y);
|
|
615
|
+
this._removeTouch(id);
|
|
616
|
+
}
|
|
617
|
+
/** Access the gamepad state at `index`. Allocates on first access. */
|
|
618
|
+
gamepad(index) {
|
|
619
|
+
let pad = this._gamepads[index];
|
|
620
|
+
if (!pad) {
|
|
621
|
+
pad = createGamepadState(index);
|
|
622
|
+
this._gamepads[index] = pad;
|
|
623
|
+
}
|
|
624
|
+
return pad;
|
|
625
|
+
}
|
|
626
|
+
/** Dead zone in [0, 1). Axes at or below this magnitude read as 0. */
|
|
627
|
+
setGamepadDeadZone(deadZone) {
|
|
628
|
+
this._gamepadDeadZone = Math.max(0, Math.min(.95, deadZone));
|
|
629
|
+
}
|
|
630
|
+
/** Install a remap table for a specific gamepad index. Pass `null` to clear. */
|
|
631
|
+
setGamepadRemap(index, table) {
|
|
632
|
+
if (table === null) this._gamepadRemaps.delete(index);
|
|
633
|
+
else this._gamepadRemaps.set(index, table);
|
|
634
|
+
}
|
|
635
|
+
/** Poll all gamepads from the given source, applying remap + dead zones. */
|
|
636
|
+
pollGamepads(source) {
|
|
637
|
+
const snapshots = source.poll();
|
|
638
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
639
|
+
const pad = this.gamepad(i);
|
|
640
|
+
const snap = snapshots[i] ?? null;
|
|
641
|
+
updateGamepadFromSnapshot(pad, snap !== null ? this._gamepadRemaps.get(i) ? applyRemap(snap, this._gamepadRemaps.get(i)) : snap : null, this._gamepadDeadZone);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
_addOrUpdateTouch(id, x, y, isDown) {
|
|
645
|
+
let touch = this._touchMap.get(id);
|
|
646
|
+
if (!touch) {
|
|
647
|
+
touch = {
|
|
648
|
+
id,
|
|
649
|
+
position: {
|
|
650
|
+
x,
|
|
651
|
+
y
|
|
652
|
+
},
|
|
653
|
+
isDown
|
|
654
|
+
};
|
|
655
|
+
this._touchMap.set(id, touch);
|
|
656
|
+
this.touches.push(touch);
|
|
657
|
+
} else {
|
|
658
|
+
touch.position.x = x;
|
|
659
|
+
touch.position.y = y;
|
|
660
|
+
touch.isDown = isDown;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
_removeTouch(id) {
|
|
664
|
+
const touch = this._touchMap.get(id);
|
|
665
|
+
if (!touch) return;
|
|
666
|
+
this._touchMap.delete(id);
|
|
667
|
+
const idx = this.touches.indexOf(touch);
|
|
668
|
+
if (idx !== -1) this.touches.splice(idx, 1);
|
|
669
|
+
}
|
|
296
670
|
};
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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;
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/virtual-joystick.ts
|
|
673
|
+
const DEFAULT_DEAD_ZONE = 10;
|
|
674
|
+
const DEFAULT_MAX_RADIUS = 50;
|
|
675
|
+
/** Create a default joystick state. */
|
|
445
676
|
function createJoystickState() {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
677
|
+
return {
|
|
678
|
+
active: false,
|
|
679
|
+
direction: {
|
|
680
|
+
x: 0,
|
|
681
|
+
y: 0
|
|
682
|
+
},
|
|
683
|
+
magnitude: 0,
|
|
684
|
+
offset: {
|
|
685
|
+
x: 0,
|
|
686
|
+
y: 0
|
|
687
|
+
}
|
|
688
|
+
};
|
|
452
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Update joystick state from a touch event.
|
|
692
|
+
*
|
|
693
|
+
* @param state The joystick state to update.
|
|
694
|
+
* @param centerX The joystick center X in screen pixels.
|
|
695
|
+
* @param centerY The joystick center Y in screen pixels.
|
|
696
|
+
* @param touchX The current touch X in screen pixels.
|
|
697
|
+
* @param touchY The current touch Y in screen pixels.
|
|
698
|
+
* @param config Joystick configuration.
|
|
699
|
+
*/
|
|
453
700
|
function updateJoystick(state, centerX, centerY, touchX, touchY, config) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
701
|
+
const deadZone = config?.deadZone ?? DEFAULT_DEAD_ZONE;
|
|
702
|
+
const maxRadius = config?.maxRadius ?? DEFAULT_MAX_RADIUS;
|
|
703
|
+
const dx = touchX - centerX;
|
|
704
|
+
const dy = touchY - centerY;
|
|
705
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
706
|
+
state.active = true;
|
|
707
|
+
if (dist < deadZone) {
|
|
708
|
+
state.direction.x = 0;
|
|
709
|
+
state.direction.y = 0;
|
|
710
|
+
state.magnitude = 0;
|
|
711
|
+
state.offset.x = dx;
|
|
712
|
+
state.offset.y = dy;
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const clampedDist = Math.min(dist, maxRadius);
|
|
716
|
+
const normalizedDist = (clampedDist - deadZone) / (maxRadius - deadZone);
|
|
717
|
+
const invDist = 1 / dist;
|
|
718
|
+
state.direction.x = dx * invDist;
|
|
719
|
+
state.direction.y = dy * invDist;
|
|
720
|
+
state.magnitude = Math.min(normalizedDist, 1);
|
|
721
|
+
const scale = clampedDist / dist;
|
|
722
|
+
state.offset.x = dx * scale;
|
|
723
|
+
state.offset.y = dy * scale;
|
|
477
724
|
}
|
|
725
|
+
/** Reset joystick state when touch is released. */
|
|
478
726
|
function releaseJoystick(state) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
727
|
+
state.active = false;
|
|
728
|
+
state.direction.x = 0;
|
|
729
|
+
state.direction.y = 0;
|
|
730
|
+
state.magnitude = 0;
|
|
731
|
+
state.offset.x = 0;
|
|
732
|
+
state.offset.y = 0;
|
|
485
733
|
}
|
|
734
|
+
//#endregion
|
|
735
|
+
export { GamepadAxis, GamepadButton, GestureDetector, InputManager, applyRemap, attachKeyboardListeners, createButtonState, createGamepadState, createIdentityRemap, createJoystickState, createKeyboard, createPointer, createWebGamepadSource, getAxis, isActionDown, isButtonDown, loadRemap, pointerDown, pointerMove, pointerUp, pressButton, releaseButton, releaseJoystick, resetButtonFrame, resetGamepadFrame, resetKeyboardFrame, resetPointerFrame, saveRemap, updateGamepadFromSnapshot, updateJoystick, wasActionPressed, wasActionReleased, wasButtonPressed, wasButtonReleased };
|
|
486
736
|
|
|
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
737
|
//# sourceMappingURL=index.mjs.map
|