@flare-engine/input 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.d.mts +37 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +37 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["Signal"],"sources":["../src/gamepad.ts","../src/gamepad-source.ts","../src/gestures.ts","../src/virtual-button.ts","../src/keyboard.ts","../src/pointer.ts","../src/remap.ts","../src/input-manager.ts","../src/virtual-joystick.ts"],"sourcesContent":["/**\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 * 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?.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","/** 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","import {\n createButtonState,\n pressButton,\n releaseButton,\n resetButtonFrame,\n type VirtualButtonState,\n} from './virtual-button.js';\n\n/** Minimal keyboard event shape — no DOM type dep. */\nexport interface KeyboardEventLike {\n readonly code: string;\n readonly type: string;\n /** Optional — real DOM events have it; synthetic feeds may not. */\n preventDefault?(): void;\n}\n\n/** Minimal event target shape for attaching keyboard listeners. */\nexport interface KeyboardEventTargetLike {\n addEventListener(type: string, handler: (ev: KeyboardEventLike) => void): void;\n removeEventListener(type: string, handler: (ev: KeyboardEventLike) => void): void;\n}\n\n/**\n * Buffered per-frame keyboard state.\n *\n * Edge flags (`wasPressed`, `wasReleased`) clear each frame via `reset()`.\n * `keyDown` / `keyUp` are the platform-neutral injection points: web feeds\n * them via `attachKeyboardListeners`; mobile/RN feeds them from a\n * hardware-keyboard native module or an on-screen source.\n */\nexport interface KeyboardState {\n /** True while the key is physically held. */\n isDown(code: string): boolean;\n /** True only on the frame the key was pressed. */\n wasPressed(code: string): boolean;\n /** True only on the frame the key was released. */\n wasReleased(code: string): boolean;\n /** Platform-neutral injection. OS auto-repeat is de-edged by the pressButton guard. */\n keyDown(code: string): void;\n /** Platform-neutral injection. */\n keyUp(code: string): void;\n /** Frame-start edge clear. Zero-alloc. */\n reset(): void;\n /** Blur/stuck-key guard: emits a release edge for every held code, then drops held. */\n clear(): void;\n}\n\n/** Create a fresh keyboard state. */\nexport function createKeyboard(): KeyboardState {\n const states: VirtualButtonState[] = [];\n const codeIndex = new Map<string, number>();\n\n return {\n isDown(code: string): boolean {\n const i = codeIndex.get(code);\n if (i === undefined) return false;\n return states[i]?.isPressed ?? false;\n },\n\n wasPressed(code: string): boolean {\n const i = codeIndex.get(code);\n if (i === undefined) return false;\n return states[i]?.justPressed ?? false;\n },\n\n wasReleased(code: string): boolean {\n const i = codeIndex.get(code);\n if (i === undefined) return false;\n return states[i]?.justReleased ?? false;\n },\n\n keyDown(code: string): void {\n let i = codeIndex.get(code);\n if (i === undefined) {\n i = states.length;\n states.push(createButtonState());\n codeIndex.set(code, i);\n }\n pressButton(states[i]!);\n },\n\n keyUp(code: string): void {\n const i = codeIndex.get(code);\n if (i === undefined) return;\n releaseButton(states[i]!);\n },\n\n reset(): void {\n for (let i = 0; i < states.length; i++) {\n resetButtonFrame(states[i]!);\n }\n },\n\n clear(): void {\n for (let i = 0; i < states.length; i++) {\n releaseButton(states[i]!);\n }\n },\n };\n}\n\n/** Free-function parity with `resetPointerFrame`. Delegates to `kb.reset()`. */\nexport function resetKeyboardFrame(kb: KeyboardState): void {\n kb.reset();\n}\n\n/**\n * Web turnkey adapter: registers `keydown`, `keyup`, and `blur` listeners on\n * `target`. `blur` calls `kb.clear()` (stuck-key guard). Returns a detach fn.\n *\n * `target` is structural `{ addEventListener, removeEventListener }` — no DOM\n * types dep. On web, pass `window`; for mobile/RN, feed `keyDown`/`keyUp`\n * directly from a hardware-keyboard native module or an on-screen source.\n *\n * Does NOT call `preventDefault` — bare use lets Space/arrows scroll and Tab\n * move DOM focus. For game-mode input, attach your own listener that calls\n * `preventDefault` for game keys and forwards `kb.keyDown(ev.code)`.\n */\nexport function attachKeyboardListeners(\n kb: KeyboardState,\n target: KeyboardEventTargetLike,\n): () => void {\n const onKeyDown = (ev: KeyboardEventLike): void => {\n kb.keyDown(ev.code);\n };\n const onKeyUp = (ev: KeyboardEventLike): void => {\n kb.keyUp(ev.code);\n };\n const onBlur = (_ev: KeyboardEventLike): void => {\n kb.clear();\n };\n\n target.addEventListener('keydown', onKeyDown);\n target.addEventListener('keyup', onKeyUp);\n target.addEventListener('blur', onBlur);\n\n return (): void => {\n target.removeEventListener('keydown', onKeyDown);\n target.removeEventListener('keyup', onKeyUp);\n target.removeEventListener('blur', onBlur);\n };\n}\n\n/**\n * Action → code binding map. Keys are action names; values are arrays of\n * `KeyboardEvent.code` strings. Multiple codes OR-reduce (any bound key\n * triggers the action).\n */\nexport type KeyBindings = Readonly<Record<string, readonly string[]>>;\n\n/** True if any bound code for `action` is currently held. */\nexport function isActionDown(kb: KeyboardState, bindings: KeyBindings, action: string): boolean {\n const codes = bindings[action];\n if (!codes) return false;\n for (let i = 0; i < codes.length; i++) {\n if (kb.isDown(codes[i]!)) return true;\n }\n return false;\n}\n\n/** True if any bound code for `action` was pressed this frame. */\nexport function wasActionPressed(\n kb: KeyboardState,\n bindings: KeyBindings,\n action: string,\n): boolean {\n const codes = bindings[action];\n if (!codes) return false;\n for (let i = 0; i < codes.length; i++) {\n if (kb.wasPressed(codes[i]!)) return true;\n }\n return false;\n}\n\n/**\n * True if any bound code for `action` was released this frame AND no bound\n * code is still held. Release fires only when the last held bound key lifts.\n */\nexport function wasActionReleased(\n kb: KeyboardState,\n bindings: KeyBindings,\n action: string,\n): boolean {\n const codes = bindings[action];\n if (!codes) return false;\n let anyReleased = false;\n for (let i = 0; i < codes.length; i++) {\n if (kb.isDown(codes[i]!)) return false;\n if (kb.wasReleased(codes[i]!)) anyReleased = true;\n }\n return anyReleased;\n}\n","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","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 {\n createGamepadState,\n type GamepadState,\n resetGamepadFrame,\n updateGamepadFromSnapshot,\n} from './gamepad.js';\nimport type { GamepadSource } from './gamepad-source.js';\nimport { createKeyboard, type KeyboardState, resetKeyboardFrame } from './keyboard.js';\nimport {\n createPointer,\n type Pointer,\n pointerDown,\n pointerMove,\n pointerUp,\n resetPointerFrame,\n type TouchPoint,\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 /** Buffered keyboard state. Feed via `attachKeyboardListeners` (web) or `keyboard.keyDown`/`keyUp` (mobile/RN). */\n readonly keyboard: KeyboardState = createKeyboard();\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 resetKeyboardFrame(this.keyboard);\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 { 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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["G","Signal"],"sources":["../src/gamepad.ts","../src/gamepad-source.ts","../src/gestures.ts","../src/virtual-button.ts","../src/keyboard.ts","../src/pointer.ts","../src/remap.ts","../src/input-manager.ts","../src/virtual-joystick.ts"],"sourcesContent":["/**\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/**\n * Dev-flag access for the dead-zone guard, which runs per axis per frame. Read\n * as a `globalThis` property rather than a bare `__DEV__` identifier for the\n * reason measured in PERF-10 (~0.46 ns vs ~270 ns on JSC).\n */\nconst G = globalThis as { __DEV__?: boolean };\n\n/** Warn at most once per process; a bad dead zone repeats every frame. */\nlet _warnedDeadZone = false;\n\n/** Largest dead zone that still leaves the rescale well-defined. */\nconst MAX_DEAD_ZONE = 0.99;\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 justPressed: boolean;\n /** True for exactly one frame after a release. */\n justReleased: 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, justPressed: false, justReleased: 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 // The documented contract is [0, 1). At exactly 1 the rescale below divides by\n // zero and returns ±Infinity for any out-of-range axis, which then propagates\n // into every consumer reading the stick. Correct rather than propagate.\n let dz = deadZone;\n if (!(dz >= 0 && dz < 1)) {\n if (G.__DEV__ && !_warnedDeadZone) {\n _warnedDeadZone = true;\n console.warn(\n `[flare-engine/input] deadZone=${deadZone} is outside [0, 1); clamping. ` +\n 'Values at or above 1 would suppress the axis entirely.',\n );\n }\n dz = dz >= 1 ? MAX_DEAD_ZONE : 0;\n }\n const abs = Math.abs(value);\n // `!(abs > dz)` rather than `abs <= dz` so a non-finite axis returns 0 instead\n // of falling through to produce NaN.\n if (!(abs > dz)) 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 - dz) / (1 - dz));\n}\n\n/**\n * Apply a fresh snapshot to an existing `GamepadState` in place. Derived\n * `justPressed` / `justReleased` 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.justPressed = false;\n b.justReleased = 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 wasDown = prev.pressed;\n prev.pressed = src.pressed;\n prev.value = src.value;\n prev.justPressed = src.pressed && !wasDown;\n prev.justReleased = !src.pressed && wasDown;\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.justPressed = false;\n b.justReleased = 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.justPressed = false;\n b.justReleased = 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]?.justPressed ?? false;\n}\n\nexport function wasButtonReleased(state: GamepadState, button: number): boolean {\n return state.buttons[button]?.justReleased ?? 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 * 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 *\n * **Freshness contract:** each call returns fresh objects. An implementation\n * must not reuse or mutate a snapshot (or its `buttons`/`axes` arrays) that a\n * previous `poll()` call returned. A caller may therefore retain a returned\n * snapshot across frames without it changing out from under them.\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?.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 /** Elapsed time (seconds) since `pointerDown` for the in-progress press. */\n private _elapsed = 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._elapsed = 0;\n this._isTracking = true;\n this._longPressFired = false;\n }\n\n /**\n * Feed a pointer-up event.\n *\n * `elapsedSinceDown` is optional: pass it when the host tracks its own\n * timestamps (e.g. event-driven, no per-frame `update()` calls); omit it\n * to use the elapsed time already accumulated via `update(dt, ...)`. Either\n * way there is one clock per press — passing it here re-syncs the same\n * accumulator `update()` advances, rather than racing it. If a long press\n * already fired for this press, tap/swipe are suppressed so one press\n * yields exactly one gesture.\n */\n pointerUp(x: number, y: number, elapsedSinceDown?: number): void {\n if (!this._isTracking) return;\n this._isTracking = false;\n if (elapsedSinceDown !== undefined) this._elapsed = elapsedSinceDown;\n if (this._longPressFired) return;\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 = this._elapsed;\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._elapsed += dt;\n\n // Long press detection\n if (!this._longPressFired && this._elapsed >= 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","/** 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","import {\n createButtonState,\n pressButton,\n releaseButton,\n resetButtonFrame,\n type VirtualButtonState,\n} from './virtual-button.js';\n\n/** Minimal keyboard event shape — no DOM type dep. */\nexport interface KeyboardEventLike {\n readonly code: string;\n readonly type: string;\n /** Optional — real DOM events have it; synthetic feeds may not. */\n preventDefault?(): void;\n}\n\n/** Minimal event target shape for attaching keyboard listeners. */\nexport interface KeyboardEventTargetLike {\n addEventListener(type: string, handler: (ev: KeyboardEventLike) => void): void;\n removeEventListener(type: string, handler: (ev: KeyboardEventLike) => void): void;\n}\n\n/**\n * Buffered per-frame keyboard state.\n *\n * Edge flags (`wasPressed`, `wasReleased`) clear each frame via `reset()`.\n * `keyDown` / `keyUp` are the platform-neutral injection points: web feeds\n * them via `attachKeyboardListeners`; mobile/RN feeds them from a\n * hardware-keyboard native module or an on-screen source.\n */\nexport interface KeyboardState {\n /** True while the key is physically held. */\n isDown(code: string): boolean;\n /** True only on the frame the key was pressed. */\n wasPressed(code: string): boolean;\n /** True only on the frame the key was released. */\n wasReleased(code: string): boolean;\n /** Platform-neutral injection. OS auto-repeat is de-edged by the pressButton guard. */\n keyDown(code: string): void;\n /** Platform-neutral injection. */\n keyUp(code: string): void;\n /** Frame-start edge clear. Zero-alloc. */\n reset(): void;\n /** Blur/stuck-key guard: emits a release edge for every held code, then drops held. */\n clear(): void;\n}\n\n/** Create a fresh keyboard state. */\nexport function createKeyboard(): KeyboardState {\n const states: VirtualButtonState[] = [];\n const codeIndex = new Map<string, number>();\n\n return {\n isDown(code: string): boolean {\n const i = codeIndex.get(code);\n if (i === undefined) return false;\n return states[i]?.isPressed ?? false;\n },\n\n wasPressed(code: string): boolean {\n const i = codeIndex.get(code);\n if (i === undefined) return false;\n return states[i]?.justPressed ?? false;\n },\n\n wasReleased(code: string): boolean {\n const i = codeIndex.get(code);\n if (i === undefined) return false;\n return states[i]?.justReleased ?? false;\n },\n\n keyDown(code: string): void {\n let i = codeIndex.get(code);\n if (i === undefined) {\n i = states.length;\n states.push(createButtonState());\n codeIndex.set(code, i);\n }\n pressButton(states[i]!);\n },\n\n keyUp(code: string): void {\n const i = codeIndex.get(code);\n if (i === undefined) return;\n releaseButton(states[i]!);\n },\n\n reset(): void {\n for (let i = 0; i < states.length; i++) {\n resetButtonFrame(states[i]!);\n }\n },\n\n clear(): void {\n for (let i = 0; i < states.length; i++) {\n releaseButton(states[i]!);\n }\n },\n };\n}\n\n/**\n * Web turnkey adapter: registers `keydown`, `keyup`, and `blur` listeners on\n * `target`. `blur` calls `kb.clear()` (stuck-key guard). Returns a detach fn.\n *\n * `target` is structural `{ addEventListener, removeEventListener }` — no DOM\n * types dep. On web, pass `window`; for mobile/RN, feed `keyDown`/`keyUp`\n * directly from a hardware-keyboard native module or an on-screen source.\n *\n * Does NOT call `preventDefault` — bare use lets Space/arrows scroll and Tab\n * move DOM focus. For game-mode input, attach your own listener that calls\n * `preventDefault` for game keys and forwards `kb.keyDown(ev.code)`.\n */\nexport function attachKeyboardListeners(\n kb: KeyboardState,\n target: KeyboardEventTargetLike,\n): () => void {\n const onKeyDown = (ev: KeyboardEventLike): void => {\n kb.keyDown(ev.code);\n };\n const onKeyUp = (ev: KeyboardEventLike): void => {\n kb.keyUp(ev.code);\n };\n const onBlur = (_ev: KeyboardEventLike): void => {\n kb.clear();\n };\n\n target.addEventListener('keydown', onKeyDown);\n target.addEventListener('keyup', onKeyUp);\n target.addEventListener('blur', onBlur);\n\n return (): void => {\n target.removeEventListener('keydown', onKeyDown);\n target.removeEventListener('keyup', onKeyUp);\n target.removeEventListener('blur', onBlur);\n };\n}\n\n/**\n * Action → code binding map. Keys are action names; values are arrays of\n * `KeyboardEvent.code` strings. Multiple codes OR-reduce (any bound key\n * triggers the action).\n */\nexport type KeyBindings = Readonly<Record<string, readonly string[]>>;\n\n/** True if any bound code for `action` is currently held. */\nexport function isActionDown(kb: KeyboardState, bindings: KeyBindings, action: string): boolean {\n const codes = bindings[action];\n if (!codes) return false;\n for (let i = 0; i < codes.length; i++) {\n if (kb.isDown(codes[i]!)) return true;\n }\n return false;\n}\n\n/** True if any bound code for `action` was pressed this frame. */\nexport function wasActionPressed(\n kb: KeyboardState,\n bindings: KeyBindings,\n action: string,\n): boolean {\n const codes = bindings[action];\n if (!codes) return false;\n for (let i = 0; i < codes.length; i++) {\n if (kb.wasPressed(codes[i]!)) return true;\n }\n return false;\n}\n\n/**\n * True if any bound code for `action` was released this frame AND no bound\n * code is still held. Release fires only when the last held bound key lifts.\n */\nexport function wasActionReleased(\n kb: KeyboardState,\n bindings: KeyBindings,\n action: string,\n): boolean {\n const codes = bindings[action];\n if (!codes) return false;\n let anyReleased = false;\n for (let i = 0; i < codes.length; i++) {\n if (kb.isDown(codes[i]!)) return false;\n if (kb.wasReleased(codes[i]!)) anyReleased = true;\n }\n return anyReleased;\n}\n","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","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 {\n createGamepadState,\n type GamepadState,\n resetGamepadFrame,\n updateGamepadFromSnapshot,\n} from './gamepad.js';\nimport type { GamepadSource } from './gamepad-source.js';\nimport { createKeyboard, type KeyboardState } from './keyboard.js';\nimport {\n createPointer,\n type Pointer,\n pointerDown,\n pointerMove,\n pointerUp,\n resetPointerFrame,\n type TouchPoint,\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 /**\n * Primary pointer — the touch that started the current gesture (first\n * `onPointerDown` while no touch was active), or the mouse.\n *\n * Once assigned, the primary id stays fixed to that touch until it lifts;\n * it does NOT transfer to a remaining finger. While a second finger is\n * down, its events reach only `touches` — `pointer` neither moves nor\n * releases because of it. When the primary lifts, `pointer.isDown` goes\n * false even if other fingers remain down, and a new primary is assigned\n * only once every touch has lifted and a fresh `onPointerDown` arrives.\n * This keeps `pointer` a single continuous stream with no mid-gesture\n * teleport; read `touches` for anything beyond one finger.\n */\n readonly pointer: Pointer = createPointer();\n\n /** Buffered keyboard state. Feed via `attachKeyboardListeners` (web) or `keyboard.keyDown`/`keyUp` (mobile/RN). */\n readonly keyboard: KeyboardState = createKeyboard();\n\n /** All active touch points. */\n readonly touches: TouchPoint[] = [];\n\n private _touchMap = new Map<number, TouchPoint>();\n private _primaryTouchId: number | null = null;\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 this.keyboard.reset();\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 if (this._touchMap.size === 0) {\n this._primaryTouchId = id;\n }\n if (id === this._primaryTouchId) {\n pointerDown(this.pointer, x, y);\n }\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 if (id === this._primaryTouchId) {\n pointerMove(this.pointer, x, y);\n }\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 if (id === this._primaryTouchId) {\n pointerUp(this.pointer, x, y);\n this._primaryTouchId = null;\n }\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 { Vec2 } from '@flare-engine/math';\n\n/**\n * Dev-flag access for the config guard in `updateJoystick`, which runs once per\n * frame while a stick is held. Read as a `globalThis` property, not as a bare\n * `__DEV__` identifier: on JSC an unresolvable global costs ~270 ns per\n * evaluation where a property miss is inline-cached at ~0.46 ns. Same reasoning\n * as `@flare-engine/core`'s `Pool` — see PERF-10.\n */\nconst G = globalThis as { __DEV__?: boolean };\n\n/** Warn at most once per process; a bad config repeats every frame. */\nlet _warnedConfig = false;\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 let deadZone = config?.deadZone ?? DEFAULT_DEAD_ZONE;\n let maxRadius = config?.maxRadius ?? DEFAULT_MAX_RADIUS;\n\n // Out-of-contract config is corrected rather than propagated. Left alone, a\n // NaN deadZone defeats the early return below and a maxRadius equal to\n // deadZone divides by zero at `normalizedDist` — either way `state.direction`\n // becomes NaN and, because the state is reused across frames, the stick never\n // recovers. The whole check sits behind one comparison on the healthy path.\n if (!(deadZone >= 0 && maxRadius > deadZone)) {\n if (G.__DEV__ && !_warnedConfig) {\n _warnedConfig = true;\n console.warn(\n `[flare-engine/input] updateJoystick: invalid config (deadZone=${deadZone}, maxRadius=${maxRadius}); ` +\n 'expected 0 <= deadZone < maxRadius. Falling back to defaults for this call.',\n );\n }\n if (!(deadZone >= 0)) deadZone = DEFAULT_DEAD_ZONE;\n if (!(maxRadius > deadZone)) maxRadius = deadZone + DEFAULT_MAX_RADIUS;\n }\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 // `!(dist >= deadZone)` rather than `dist < deadZone` so a non-finite touch\n // coordinate takes the centred branch instead of falling through to the maths.\n if (!(dist >= deadZone)) {\n state.direction.x = 0;\n state.direction.y = 0;\n state.magnitude = 0;\n // dist is non-finite only when a touch coordinate was, in which case dx/dy\n // are too — report a centred offset rather than writing NaN into the state.\n const centred = dist === 0 || !Number.isFinite(dist);\n state.offset.x = centred ? 0 : dx;\n state.offset.y = centred ? 0 : 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. `dist` is > 0 here —\n // a zero distance cannot pass the guard above once deadZone >= 0 — but the\n // reciprocal is guarded anyway, because `deadZone: 0` makes `dist === 0`\n // reachable and `1 / 0` would put Infinity into direction.\n const invDist = dist > 0 ? 1 / dist : 0;\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 = dist > 0 ? clampedDist / dist : 0;\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"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,16 @@ import { Signal } from "@flare-engine/events";
|
|
|
7
7
|
* default. Callers can translate raw indices via `remap.ts` before feeding
|
|
8
8
|
* snapshots into `updateGamepadFromSnapshot`.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Dev-flag access for the dead-zone guard, which runs per axis per frame. Read
|
|
12
|
+
* as a `globalThis` property rather than a bare `__DEV__` identifier for the
|
|
13
|
+
* reason measured in PERF-10 (~0.46 ns vs ~270 ns on JSC).
|
|
14
|
+
*/
|
|
15
|
+
const G$1 = globalThis;
|
|
16
|
+
/** Warn at most once per process; a bad dead zone repeats every frame. */
|
|
17
|
+
let _warnedDeadZone = false;
|
|
18
|
+
/** Largest dead zone that still leaves the rescale well-defined. */
|
|
19
|
+
const MAX_DEAD_ZONE = .99;
|
|
10
20
|
/** W3C Standard Gamepad button indices. */
|
|
11
21
|
const GamepadButton = {
|
|
12
22
|
A: 0,
|
|
@@ -39,8 +49,8 @@ function createGamepadState(index = 0, buttonCount = 17, axisCount = 4) {
|
|
|
39
49
|
for (let i = 0; i < buttonCount; i++) buttons[i] = {
|
|
40
50
|
pressed: false,
|
|
41
51
|
value: 0,
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
justPressed: false,
|
|
53
|
+
justReleased: false
|
|
44
54
|
};
|
|
45
55
|
return {
|
|
46
56
|
connected: false,
|
|
@@ -52,13 +62,21 @@ function createGamepadState(index = 0, buttonCount = 17, axisCount = 4) {
|
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
function applyDeadZone(value, deadZone) {
|
|
65
|
+
let dz = deadZone;
|
|
66
|
+
if (!(dz >= 0 && dz < 1)) {
|
|
67
|
+
if (G$1.__DEV__ && !_warnedDeadZone) {
|
|
68
|
+
_warnedDeadZone = true;
|
|
69
|
+
console.warn(`[flare-engine/input] deadZone=${deadZone} is outside [0, 1); clamping. Values at or above 1 would suppress the axis entirely.`);
|
|
70
|
+
}
|
|
71
|
+
dz = dz >= 1 ? MAX_DEAD_ZONE : 0;
|
|
72
|
+
}
|
|
55
73
|
const abs = Math.abs(value);
|
|
56
|
-
if (abs
|
|
57
|
-
return (value < 0 ? -1 : 1) * ((abs -
|
|
74
|
+
if (!(abs > dz)) return 0;
|
|
75
|
+
return (value < 0 ? -1 : 1) * ((abs - dz) / (1 - dz));
|
|
58
76
|
}
|
|
59
77
|
/**
|
|
60
78
|
* Apply a fresh snapshot to an existing `GamepadState` in place. Derived
|
|
61
|
-
* `
|
|
79
|
+
* `justPressed` / `justReleased` flags are recomputed from the
|
|
62
80
|
* previous `pressed` value.
|
|
63
81
|
*
|
|
64
82
|
* `deadZone` in [0, 1). Values at or below are treated as 0.
|
|
@@ -72,8 +90,8 @@ function updateGamepadFromSnapshot(state, snapshot, deadZone = 0) {
|
|
|
72
90
|
for (let i = 0; i < state.buttons.length; i++) {
|
|
73
91
|
const b = state.buttons[i];
|
|
74
92
|
if (!b) continue;
|
|
75
|
-
b.
|
|
76
|
-
b.
|
|
93
|
+
b.justPressed = false;
|
|
94
|
+
b.justReleased = b.pressed;
|
|
77
95
|
b.pressed = false;
|
|
78
96
|
b.value = 0;
|
|
79
97
|
}
|
|
@@ -92,17 +110,17 @@ function updateGamepadFromSnapshot(state, snapshot, deadZone = 0) {
|
|
|
92
110
|
const prev = state.buttons[i];
|
|
93
111
|
const src = srcButtons[i];
|
|
94
112
|
if (!prev || !src) continue;
|
|
95
|
-
const
|
|
113
|
+
const wasDown = prev.pressed;
|
|
96
114
|
prev.pressed = src.pressed;
|
|
97
115
|
prev.value = src.value;
|
|
98
|
-
prev.
|
|
99
|
-
prev.
|
|
116
|
+
prev.justPressed = src.pressed && !wasDown;
|
|
117
|
+
prev.justReleased = !src.pressed && wasDown;
|
|
100
118
|
}
|
|
101
119
|
for (let i = bLen; i < state.buttons.length; i++) {
|
|
102
120
|
const b = state.buttons[i];
|
|
103
121
|
if (!b) continue;
|
|
104
|
-
b.
|
|
105
|
-
b.
|
|
122
|
+
b.justPressed = false;
|
|
123
|
+
b.justReleased = b.pressed;
|
|
106
124
|
b.pressed = false;
|
|
107
125
|
b.value = 0;
|
|
108
126
|
}
|
|
@@ -118,8 +136,8 @@ function clearFrameFlags(state) {
|
|
|
118
136
|
for (let i = 0; i < state.buttons.length; i++) {
|
|
119
137
|
const b = state.buttons[i];
|
|
120
138
|
if (!b) continue;
|
|
121
|
-
b.
|
|
122
|
-
b.
|
|
139
|
+
b.justPressed = false;
|
|
140
|
+
b.justReleased = false;
|
|
123
141
|
}
|
|
124
142
|
}
|
|
125
143
|
function anyButtonDown(state) {
|
|
@@ -130,10 +148,10 @@ function isButtonDown(state, button) {
|
|
|
130
148
|
return state.buttons[button]?.pressed ?? false;
|
|
131
149
|
}
|
|
132
150
|
function wasButtonPressed(state, button) {
|
|
133
|
-
return state.buttons[button]?.
|
|
151
|
+
return state.buttons[button]?.justPressed ?? false;
|
|
134
152
|
}
|
|
135
153
|
function wasButtonReleased(state, button) {
|
|
136
|
-
return state.buttons[button]?.
|
|
154
|
+
return state.buttons[button]?.justReleased ?? false;
|
|
137
155
|
}
|
|
138
156
|
function getAxis(state, axis) {
|
|
139
157
|
return state.axes[axis] ?? 0;
|
|
@@ -205,7 +223,8 @@ var GestureDetector = class {
|
|
|
205
223
|
x: 0,
|
|
206
224
|
y: 0
|
|
207
225
|
};
|
|
208
|
-
|
|
226
|
+
/** Elapsed time (seconds) since `pointerDown` for the in-progress press. */
|
|
227
|
+
_elapsed = 0;
|
|
209
228
|
_isTracking = false;
|
|
210
229
|
_longPressFired = false;
|
|
211
230
|
_pinchStartDistance = 0;
|
|
@@ -223,18 +242,30 @@ var GestureDetector = class {
|
|
|
223
242
|
x,
|
|
224
243
|
y
|
|
225
244
|
};
|
|
226
|
-
this.
|
|
245
|
+
this._elapsed = 0;
|
|
227
246
|
this._isTracking = true;
|
|
228
247
|
this._longPressFired = false;
|
|
229
248
|
}
|
|
230
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Feed a pointer-up event.
|
|
251
|
+
*
|
|
252
|
+
* `elapsedSinceDown` is optional: pass it when the host tracks its own
|
|
253
|
+
* timestamps (e.g. event-driven, no per-frame `update()` calls); omit it
|
|
254
|
+
* to use the elapsed time already accumulated via `update(dt, ...)`. Either
|
|
255
|
+
* way there is one clock per press — passing it here re-syncs the same
|
|
256
|
+
* accumulator `update()` advances, rather than racing it. If a long press
|
|
257
|
+
* already fired for this press, tap/swipe are suppressed so one press
|
|
258
|
+
* yields exactly one gesture.
|
|
259
|
+
*/
|
|
231
260
|
pointerUp(x, y, elapsedSinceDown) {
|
|
232
261
|
if (!this._isTracking) return;
|
|
233
262
|
this._isTracking = false;
|
|
263
|
+
if (elapsedSinceDown !== void 0) this._elapsed = elapsedSinceDown;
|
|
264
|
+
if (this._longPressFired) return;
|
|
234
265
|
const dx = x - this._startPos.x;
|
|
235
266
|
const dy = y - this._startPos.y;
|
|
236
267
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
237
|
-
const duration =
|
|
268
|
+
const duration = this._elapsed;
|
|
238
269
|
if (dist >= this.swipeMinDistance && duration <= this.swipeMaxDuration) {
|
|
239
270
|
const absDx = Math.abs(dx);
|
|
240
271
|
const absDy = Math.abs(dy);
|
|
@@ -264,8 +295,8 @@ var GestureDetector = class {
|
|
|
264
295
|
*/
|
|
265
296
|
update(dt, currentX, currentY) {
|
|
266
297
|
if (!this._isTracking) return;
|
|
267
|
-
this.
|
|
268
|
-
if (!this._longPressFired && this.
|
|
298
|
+
this._elapsed += dt;
|
|
299
|
+
if (!this._longPressFired && this._elapsed >= this.longPressMinDuration) {
|
|
269
300
|
const dx = currentX - this._startPos.x;
|
|
270
301
|
const dy = currentY - this._startPos.y;
|
|
271
302
|
if (Math.sqrt(dx * dx + dy * dy) <= this.tapMaxDistance) {
|
|
@@ -382,10 +413,6 @@ function createKeyboard() {
|
|
|
382
413
|
}
|
|
383
414
|
};
|
|
384
415
|
}
|
|
385
|
-
/** Free-function parity with `resetPointerFrame`. Delegates to `kb.reset()`. */
|
|
386
|
-
function resetKeyboardFrame(kb) {
|
|
387
|
-
kb.reset();
|
|
388
|
-
}
|
|
389
416
|
/**
|
|
390
417
|
* Web turnkey adapter: registers `keydown`, `keyup`, and `blur` listeners on
|
|
391
418
|
* `target`. `blur` calls `kb.clear()` (stuck-key guard). Returns a detach fn.
|
|
@@ -580,20 +607,33 @@ function isRemapTable(value) {
|
|
|
580
607
|
* Feed events from the platform (RN touch/gesture handlers) via the on* methods.
|
|
581
608
|
*/
|
|
582
609
|
var InputManager = class {
|
|
583
|
-
/**
|
|
610
|
+
/**
|
|
611
|
+
* Primary pointer — the touch that started the current gesture (first
|
|
612
|
+
* `onPointerDown` while no touch was active), or the mouse.
|
|
613
|
+
*
|
|
614
|
+
* Once assigned, the primary id stays fixed to that touch until it lifts;
|
|
615
|
+
* it does NOT transfer to a remaining finger. While a second finger is
|
|
616
|
+
* down, its events reach only `touches` — `pointer` neither moves nor
|
|
617
|
+
* releases because of it. When the primary lifts, `pointer.isDown` goes
|
|
618
|
+
* false even if other fingers remain down, and a new primary is assigned
|
|
619
|
+
* only once every touch has lifted and a fresh `onPointerDown` arrives.
|
|
620
|
+
* This keeps `pointer` a single continuous stream with no mid-gesture
|
|
621
|
+
* teleport; read `touches` for anything beyond one finger.
|
|
622
|
+
*/
|
|
584
623
|
pointer = createPointer();
|
|
585
624
|
/** Buffered keyboard state. Feed via `attachKeyboardListeners` (web) or `keyboard.keyDown`/`keyUp` (mobile/RN). */
|
|
586
625
|
keyboard = createKeyboard();
|
|
587
626
|
/** All active touch points. */
|
|
588
627
|
touches = [];
|
|
589
628
|
_touchMap = /* @__PURE__ */ new Map();
|
|
629
|
+
_primaryTouchId = null;
|
|
590
630
|
_gamepads = [];
|
|
591
631
|
_gamepadDeadZone = .15;
|
|
592
632
|
_gamepadRemaps = /* @__PURE__ */ new Map();
|
|
593
633
|
/** Call once per frame to reset per-frame flags. */
|
|
594
634
|
update() {
|
|
595
635
|
resetPointerFrame(this.pointer);
|
|
596
|
-
|
|
636
|
+
this.keyboard.reset();
|
|
597
637
|
for (let i = 0; i < this._gamepads.length; i++) {
|
|
598
638
|
const pad = this._gamepads[i];
|
|
599
639
|
if (pad) resetGamepadFrame(pad);
|
|
@@ -601,17 +641,21 @@ var InputManager = class {
|
|
|
601
641
|
}
|
|
602
642
|
/** Feed a touch/pointer begin event. */
|
|
603
643
|
onPointerDown(id, x, y) {
|
|
604
|
-
|
|
644
|
+
if (this._touchMap.size === 0) this._primaryTouchId = id;
|
|
645
|
+
if (id === this._primaryTouchId) pointerDown(this.pointer, x, y);
|
|
605
646
|
this._addOrUpdateTouch(id, x, y, true);
|
|
606
647
|
}
|
|
607
648
|
/** Feed a touch/pointer move event. */
|
|
608
649
|
onPointerMove(id, x, y) {
|
|
609
|
-
pointerMove(this.pointer, x, y);
|
|
650
|
+
if (id === this._primaryTouchId) pointerMove(this.pointer, x, y);
|
|
610
651
|
this._addOrUpdateTouch(id, x, y, true);
|
|
611
652
|
}
|
|
612
653
|
/** Feed a touch/pointer end event. */
|
|
613
654
|
onPointerUp(id, x, y) {
|
|
614
|
-
|
|
655
|
+
if (id === this._primaryTouchId) {
|
|
656
|
+
pointerUp(this.pointer, x, y);
|
|
657
|
+
this._primaryTouchId = null;
|
|
658
|
+
}
|
|
615
659
|
this._removeTouch(id);
|
|
616
660
|
}
|
|
617
661
|
/** Access the gamepad state at `index`. Allocates on first access. */
|
|
@@ -670,6 +714,16 @@ var InputManager = class {
|
|
|
670
714
|
};
|
|
671
715
|
//#endregion
|
|
672
716
|
//#region src/virtual-joystick.ts
|
|
717
|
+
/**
|
|
718
|
+
* Dev-flag access for the config guard in `updateJoystick`, which runs once per
|
|
719
|
+
* frame while a stick is held. Read as a `globalThis` property, not as a bare
|
|
720
|
+
* `__DEV__` identifier: on JSC an unresolvable global costs ~270 ns per
|
|
721
|
+
* evaluation where a property miss is inline-cached at ~0.46 ns. Same reasoning
|
|
722
|
+
* as `@flare-engine/core`'s `Pool` — see PERF-10.
|
|
723
|
+
*/
|
|
724
|
+
const G = globalThis;
|
|
725
|
+
/** Warn at most once per process; a bad config repeats every frame. */
|
|
726
|
+
let _warnedConfig = false;
|
|
673
727
|
const DEFAULT_DEAD_ZONE = 10;
|
|
674
728
|
const DEFAULT_MAX_RADIUS = 50;
|
|
675
729
|
/** Create a default joystick state. */
|
|
@@ -698,27 +752,36 @@ function createJoystickState() {
|
|
|
698
752
|
* @param config Joystick configuration.
|
|
699
753
|
*/
|
|
700
754
|
function updateJoystick(state, centerX, centerY, touchX, touchY, config) {
|
|
701
|
-
|
|
702
|
-
|
|
755
|
+
let deadZone = config?.deadZone ?? DEFAULT_DEAD_ZONE;
|
|
756
|
+
let maxRadius = config?.maxRadius ?? DEFAULT_MAX_RADIUS;
|
|
757
|
+
if (!(deadZone >= 0 && maxRadius > deadZone)) {
|
|
758
|
+
if (G.__DEV__ && !_warnedConfig) {
|
|
759
|
+
_warnedConfig = true;
|
|
760
|
+
console.warn(`[flare-engine/input] updateJoystick: invalid config (deadZone=${deadZone}, maxRadius=${maxRadius}); expected 0 <= deadZone < maxRadius. Falling back to defaults for this call.`);
|
|
761
|
+
}
|
|
762
|
+
if (!(deadZone >= 0)) deadZone = DEFAULT_DEAD_ZONE;
|
|
763
|
+
if (!(maxRadius > deadZone)) maxRadius = deadZone + DEFAULT_MAX_RADIUS;
|
|
764
|
+
}
|
|
703
765
|
const dx = touchX - centerX;
|
|
704
766
|
const dy = touchY - centerY;
|
|
705
767
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
706
768
|
state.active = true;
|
|
707
|
-
if (dist
|
|
769
|
+
if (!(dist >= deadZone)) {
|
|
708
770
|
state.direction.x = 0;
|
|
709
771
|
state.direction.y = 0;
|
|
710
772
|
state.magnitude = 0;
|
|
711
|
-
|
|
712
|
-
state.offset.
|
|
773
|
+
const centred = dist === 0 || !Number.isFinite(dist);
|
|
774
|
+
state.offset.x = centred ? 0 : dx;
|
|
775
|
+
state.offset.y = centred ? 0 : dy;
|
|
713
776
|
return;
|
|
714
777
|
}
|
|
715
778
|
const clampedDist = Math.min(dist, maxRadius);
|
|
716
779
|
const normalizedDist = (clampedDist - deadZone) / (maxRadius - deadZone);
|
|
717
|
-
const invDist = 1 / dist;
|
|
780
|
+
const invDist = dist > 0 ? 1 / dist : 0;
|
|
718
781
|
state.direction.x = dx * invDist;
|
|
719
782
|
state.direction.y = dy * invDist;
|
|
720
783
|
state.magnitude = Math.min(normalizedDist, 1);
|
|
721
|
-
const scale = clampedDist / dist;
|
|
784
|
+
const scale = dist > 0 ? clampedDist / dist : 0;
|
|
722
785
|
state.offset.x = dx * scale;
|
|
723
786
|
state.offset.y = dy * scale;
|
|
724
787
|
}
|
|
@@ -732,6 +795,6 @@ function releaseJoystick(state) {
|
|
|
732
795
|
state.offset.y = 0;
|
|
733
796
|
}
|
|
734
797
|
//#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,
|
|
798
|
+
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, resetPointerFrame, saveRemap, updateGamepadFromSnapshot, updateJoystick, wasActionPressed, wasActionReleased, wasButtonPressed, wasButtonReleased };
|
|
736
799
|
|
|
737
800
|
//# sourceMappingURL=index.mjs.map
|