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