@dloizides/game-input 1.3.0 → 1.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/CHANGELOG.md +36 -26
- package/README.md +76 -74
- package/dist/index.d.mts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@dloizides/game-input` are documented here.
|
|
4
|
-
|
|
5
|
-
## 1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- `
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@dloizides/game-input` are documented here.
|
|
4
|
+
|
|
5
|
+
## 1.4.0
|
|
6
|
+
|
|
7
|
+
GI-HAPTICS-1 "Haptics vibrate only after a user activation".
|
|
8
|
+
|
|
9
|
+
- `createHaptics` no longer calls `navigator.vibrate` before a user gesture (Chrome blocks it and logs an
|
|
10
|
+
intervention). Gate: `navigator.userActivation.hasBeenActive` where present, else a capture-phase latch on
|
|
11
|
+
the first `pointerdown` / `keydown` / `touchstart` of `activationTarget` (new option, default `globalThis`).
|
|
12
|
+
- The enable-confirmation tick follows the same gate. Disabled and unsupported stay silent no-ops.
|
|
13
|
+
- New exports: `ActivationTarget` type, `USER_ACTIVATION_EVENTS`.
|
|
14
|
+
|
|
15
|
+
## 1.3.0
|
|
16
|
+
|
|
17
|
+
Standard Gamepad action poller (first consumer: NextGame console-dashboard UI).
|
|
18
|
+
|
|
19
|
+
- `stepPad(prev, next, repeat, now)` — pure per-frame reducer: face/shoulder buttons fire on the
|
|
20
|
+
rising edge by W3C Standard POSITION; D-pad or left stick (0.5 dead-zone, dominant axis) fires
|
|
21
|
+
once, repeats after 380 ms, then every 110 ms; release resets the repeat.
|
|
22
|
+
- `createGamepadPoller({ getPads, requestFrame, cancelFrame, onAction })` — injectable rAF loop over
|
|
23
|
+
the first connected pad; a disappearing pad resets state; `stop()` is safe from inside `onAction`.
|
|
24
|
+
- `PAD_ACTIONS`, `STANDARD_BUTTON`, `IDLE_REPEAT` + types `PadAction`, `PadSnapshot`, `RepeatState`,
|
|
25
|
+
`PadLike`, `GamepadPollerOptions`.
|
|
26
|
+
|
|
27
|
+
## 1.0.0
|
|
28
|
+
|
|
29
|
+
Initial release — the haptics primitive of the C4 games shared kit.
|
|
30
|
+
|
|
31
|
+
- `createHaptics(options)` — player-toggleable, always-safe Web Vibration API wrapper
|
|
32
|
+
(localStorage-gated, structural/injectable `navigator` + `storage`, never throws into the game loop).
|
|
33
|
+
- `HAPTIC_PATTERNS` — shared semantic vibration vocabulary (tap/select/lock/bump/navigate/success/error/loss/reveal).
|
|
34
|
+
- Zero runtime dependencies; isomorphic; dual CJS/ESM; 100% test coverage.
|
|
35
|
+
|
|
36
|
+
Extracted from Aurora's `src/input/haptics.ts`. First consumers: Aurora (re-export shim) and Morphe (new adoption).
|
package/README.md
CHANGED
|
@@ -1,74 +1,76 @@
|
|
|
1
|
-
# @dloizides/game-input
|
|
2
|
-
|
|
3
|
-
Framework-agnostic game input primitives. **v1 ships safe, player-toggleable haptics** — a thin wrapper
|
|
4
|
-
over the Web Vibration API that no-ops silently when disabled, unsupported, or blocked, so call sites can
|
|
5
|
-
fire haptics unconditionally alongside their SFX/visual feedback.
|
|
6
|
-
|
|
7
|
-
Zero runtime dependencies. Isomorphic (no DOM lib required — `navigator` and storage are structural and
|
|
8
|
-
injectable), so it works in browser games, React Native shells, and tests alike.
|
|
9
|
-
|
|
10
|
-
> **Scope note:** keyboard/gamepad normalization is intentionally *not* in v1. The games that were audited
|
|
11
|
-
> (Aurora, Morphe) implemented input with fundamentally different designs — there is no shared logic to
|
|
12
|
-
> extract yet. It will be added when a genuine 2nd same-stack consumer appears (extract-on-2nd-use).
|
|
13
|
-
|
|
14
|
-
## Install
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @dloizides/game-input
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Usage
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { createHaptics, HAPTIC_PATTERNS } from '@dloizides/game-input';
|
|
24
|
-
|
|
25
|
-
// One controller per game; the storage key namespaces the player's preference.
|
|
26
|
-
const haptics = createHaptics({ storageKey: 'aurora.haptics' });
|
|
27
|
-
|
|
28
|
-
// Fire unconditionally — disabled/unsupported just no-ops.
|
|
29
|
-
haptics.vibrate(HAPTIC_PATTERNS.tap); // menu navigation / selection
|
|
30
|
-
haptics.vibrate(HAPTIC_PATTERNS.success); // level cleared
|
|
31
|
-
haptics.vibrate([0, 14, 24, 14]); // or your own pattern
|
|
32
|
-
|
|
33
|
-
// Settings toggle — returns the new state, fires a confirming tick when enabling.
|
|
34
|
-
function onHapticsToggle(): boolean {
|
|
35
|
-
return haptics.toggle();
|
|
36
|
-
}
|
|
37
|
-
haptics.isEnabled();
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Semantic pattern vocabulary
|
|
41
|
-
|
|
42
|
-
`HAPTIC_PATTERNS` is a shared set of tuned vibration patterns keyed by intent, so every game uses the same
|
|
43
|
-
feel instead of re-picking magic durations:
|
|
44
|
-
|
|
45
|
-
| Intent | Pattern | Use |
|
|
46
|
-
|---|---|---|
|
|
47
|
-
| `tap` | `10` | menu nav / select / undo / restart |
|
|
48
|
-
| `select` | `12` | pick up a tool/charm/item |
|
|
49
|
-
| `lock` | `18` | something locks into place |
|
|
50
|
-
| `bump` | `24` | action blocked |
|
|
51
|
-
| `navigate` | `16` | portal / page turn |
|
|
52
|
-
| `success` | `[0,40,50,90]` | level/chamber cleared |
|
|
53
|
-
| `error` | `[0,14,24,14]` | fatal/destructive event |
|
|
54
|
-
| `loss` | `[0,40,50,40]` | player loses / falls |
|
|
55
|
-
| `reveal` | `[0,14,20,18,26,24]` | puzzle resolves / exit opens |
|
|
56
|
-
|
|
57
|
-
## API
|
|
58
|
-
|
|
59
|
-
- `createHaptics(options?)` → `Haptics` — `{ isEnabled, setEnabled, toggle, vibrate }`.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- `options.
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
# @dloizides/game-input
|
|
2
|
+
|
|
3
|
+
Framework-agnostic game input primitives. **v1 ships safe, player-toggleable haptics** — a thin wrapper
|
|
4
|
+
over the Web Vibration API that no-ops silently when disabled, unsupported, or blocked, so call sites can
|
|
5
|
+
fire haptics unconditionally alongside their SFX/visual feedback.
|
|
6
|
+
|
|
7
|
+
Zero runtime dependencies. Isomorphic (no DOM lib required — `navigator` and storage are structural and
|
|
8
|
+
injectable), so it works in browser games, React Native shells, and tests alike.
|
|
9
|
+
|
|
10
|
+
> **Scope note:** keyboard/gamepad normalization is intentionally *not* in v1. The games that were audited
|
|
11
|
+
> (Aurora, Morphe) implemented input with fundamentally different designs — there is no shared logic to
|
|
12
|
+
> extract yet. It will be added when a genuine 2nd same-stack consumer appears (extract-on-2nd-use).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @dloizides/game-input
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createHaptics, HAPTIC_PATTERNS } from '@dloizides/game-input';
|
|
24
|
+
|
|
25
|
+
// One controller per game; the storage key namespaces the player's preference.
|
|
26
|
+
const haptics = createHaptics({ storageKey: 'aurora.haptics' });
|
|
27
|
+
|
|
28
|
+
// Fire unconditionally — disabled/unsupported just no-ops.
|
|
29
|
+
haptics.vibrate(HAPTIC_PATTERNS.tap); // menu navigation / selection
|
|
30
|
+
haptics.vibrate(HAPTIC_PATTERNS.success); // level cleared
|
|
31
|
+
haptics.vibrate([0, 14, 24, 14]); // or your own pattern
|
|
32
|
+
|
|
33
|
+
// Settings toggle — returns the new state, fires a confirming tick when enabling.
|
|
34
|
+
function onHapticsToggle(): boolean {
|
|
35
|
+
return haptics.toggle();
|
|
36
|
+
}
|
|
37
|
+
haptics.isEnabled();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Semantic pattern vocabulary
|
|
41
|
+
|
|
42
|
+
`HAPTIC_PATTERNS` is a shared set of tuned vibration patterns keyed by intent, so every game uses the same
|
|
43
|
+
feel instead of re-picking magic durations:
|
|
44
|
+
|
|
45
|
+
| Intent | Pattern | Use |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `tap` | `10` | menu nav / select / undo / restart |
|
|
48
|
+
| `select` | `12` | pick up a tool/charm/item |
|
|
49
|
+
| `lock` | `18` | something locks into place |
|
|
50
|
+
| `bump` | `24` | action blocked |
|
|
51
|
+
| `navigate` | `16` | portal / page turn |
|
|
52
|
+
| `success` | `[0,40,50,90]` | level/chamber cleared |
|
|
53
|
+
| `error` | `[0,14,24,14]` | fatal/destructive event |
|
|
54
|
+
| `loss` | `[0,40,50,40]` | player loses / falls |
|
|
55
|
+
| `reveal` | `[0,14,20,18,26,24]` | puzzle resolves / exit opens |
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
- `createHaptics(options?)` → `Haptics` — `{ isEnabled, setEnabled, toggle, vibrate }`. Nothing vibrates
|
|
60
|
+
before a user gesture: `navigator.userActivation.hasBeenActive` where present, else the first
|
|
61
|
+
`pointerdown` / `keydown` / `touchstart` on `options.activationTarget` (default `globalThis`).
|
|
62
|
+
- `options.storageKey` (default `game.haptics`) — localStorage key for the on/off preference.
|
|
63
|
+
- `options.enableConfirmation` (default a `15`ms tick; pass `null` to disable) — fired when turning ON.
|
|
64
|
+
- `options.navigator` / `options.storage` — inject for tests/RN; default to `globalThis`.
|
|
65
|
+
- `HAPTIC_PATTERNS`, `DEFAULT_HAPTICS_STORAGE_KEY`, `DEFAULT_ENABLE_CONFIRMATION`.
|
|
66
|
+
- `createGamepadPoller({ getPads, requestFrame, cancelFrame, onAction })` → `{ start, stop }` — polls the
|
|
67
|
+
first connected W3C Standard Gamepad each frame and calls `onAction(PadAction)`. Pass
|
|
68
|
+
`() => navigator.getGamepads()`, `requestAnimationFrame`, `cancelAnimationFrame` in the browser.
|
|
69
|
+
- Buttons by POSITION: south → `confirm`, east → `back`, west → `view`, north → `mood`,
|
|
70
|
+
L/R shoulder → `pagePrev`/`pageNext` (rising edge only).
|
|
71
|
+
- D-pad or left stick (0.5 dead-zone) → `up`/`down`/`left`/`right`: fires once, repeats after 380 ms, then every 110 ms.
|
|
72
|
+
- `stepPad(prev, next, repeat, now)` — the pure per-frame reducer behind the poller; `IDLE_REPEAT`, `PAD_ACTIONS`, `STANDARD_BUTTON`.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -81,6 +81,14 @@ type VibrationPattern = number | number[];
|
|
|
81
81
|
*/
|
|
82
82
|
interface VibrationCapable {
|
|
83
83
|
vibrate?: (pattern: VibrationPattern) => boolean;
|
|
84
|
+
userActivation?: {
|
|
85
|
+
readonly hasBeenActive: boolean;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Event source whose first pointer/key/touch gesture unlocks haptics. Defaults to `globalThis`. */
|
|
89
|
+
interface ActivationTarget {
|
|
90
|
+
addEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
91
|
+
removeEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
84
92
|
}
|
|
85
93
|
/**
|
|
86
94
|
* The slice of `localStorage` haptics needs to persist the player's toggle.
|
|
@@ -103,6 +111,8 @@ interface HapticsOptions {
|
|
|
103
111
|
navigator?: VibrationCapable;
|
|
104
112
|
/** Preference store. Defaults to `globalThis.localStorage`. Inject for tests. */
|
|
105
113
|
storage?: HapticsStorage;
|
|
114
|
+
/** Gesture source used when `navigator.userActivation` is unavailable. Defaults to `globalThis`. */
|
|
115
|
+
activationTarget?: ActivationTarget;
|
|
106
116
|
}
|
|
107
117
|
/**
|
|
108
118
|
* A player-toggleable, always-safe haptics controller. Every call no-ops silently
|
|
@@ -121,11 +131,8 @@ interface Haptics {
|
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
/**
|
|
124
|
-
* Create a player-toggleable, always-safe haptics controller
|
|
125
|
-
*
|
|
126
|
-
* The on/off preference is read from (and persisted to) the configured storage key
|
|
127
|
-
* so it survives reloads. Every vibration routes through one gated call that no-ops
|
|
128
|
-
* when disabled, when no Vibration API is present, or if the platform blocks it.
|
|
134
|
+
* Create a player-toggleable, always-safe haptics controller: it never vibrates before a
|
|
135
|
+
* user gesture, while disabled, or when the Vibration API is missing or blocked.
|
|
129
136
|
*/
|
|
130
137
|
declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
131
138
|
|
|
@@ -133,6 +140,8 @@ declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
|
133
140
|
declare const DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
134
141
|
/** Default confirmation tick fired when haptics are turned ON (ms). */
|
|
135
142
|
declare const DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
143
|
+
/** DOM events that count as a user activation before `navigator.userActivation` is available. */
|
|
144
|
+
declare const USER_ACTIVATION_EVENTS: readonly ["pointerdown", "keydown", "touchstart"];
|
|
136
145
|
/** The semantic intents games map their events onto. */
|
|
137
146
|
type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success' | 'error' | 'loss' | 'reveal';
|
|
138
147
|
/**
|
|
@@ -142,4 +151,4 @@ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success'
|
|
|
142
151
|
*/
|
|
143
152
|
declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
|
|
144
153
|
|
|
145
|
-
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, stepPad };
|
|
154
|
+
export { type ActivationTarget, DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, USER_ACTIVATION_EVENTS, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, stepPad };
|
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,14 @@ type VibrationPattern = number | number[];
|
|
|
81
81
|
*/
|
|
82
82
|
interface VibrationCapable {
|
|
83
83
|
vibrate?: (pattern: VibrationPattern) => boolean;
|
|
84
|
+
userActivation?: {
|
|
85
|
+
readonly hasBeenActive: boolean;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Event source whose first pointer/key/touch gesture unlocks haptics. Defaults to `globalThis`. */
|
|
89
|
+
interface ActivationTarget {
|
|
90
|
+
addEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
91
|
+
removeEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
84
92
|
}
|
|
85
93
|
/**
|
|
86
94
|
* The slice of `localStorage` haptics needs to persist the player's toggle.
|
|
@@ -103,6 +111,8 @@ interface HapticsOptions {
|
|
|
103
111
|
navigator?: VibrationCapable;
|
|
104
112
|
/** Preference store. Defaults to `globalThis.localStorage`. Inject for tests. */
|
|
105
113
|
storage?: HapticsStorage;
|
|
114
|
+
/** Gesture source used when `navigator.userActivation` is unavailable. Defaults to `globalThis`. */
|
|
115
|
+
activationTarget?: ActivationTarget;
|
|
106
116
|
}
|
|
107
117
|
/**
|
|
108
118
|
* A player-toggleable, always-safe haptics controller. Every call no-ops silently
|
|
@@ -121,11 +131,8 @@ interface Haptics {
|
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
/**
|
|
124
|
-
* Create a player-toggleable, always-safe haptics controller
|
|
125
|
-
*
|
|
126
|
-
* The on/off preference is read from (and persisted to) the configured storage key
|
|
127
|
-
* so it survives reloads. Every vibration routes through one gated call that no-ops
|
|
128
|
-
* when disabled, when no Vibration API is present, or if the platform blocks it.
|
|
134
|
+
* Create a player-toggleable, always-safe haptics controller: it never vibrates before a
|
|
135
|
+
* user gesture, while disabled, or when the Vibration API is missing or blocked.
|
|
129
136
|
*/
|
|
130
137
|
declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
131
138
|
|
|
@@ -133,6 +140,8 @@ declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
|
133
140
|
declare const DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
134
141
|
/** Default confirmation tick fired when haptics are turned ON (ms). */
|
|
135
142
|
declare const DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
143
|
+
/** DOM events that count as a user activation before `navigator.userActivation` is available. */
|
|
144
|
+
declare const USER_ACTIVATION_EVENTS: readonly ["pointerdown", "keydown", "touchstart"];
|
|
136
145
|
/** The semantic intents games map their events onto. */
|
|
137
146
|
type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success' | 'error' | 'loss' | 'reveal';
|
|
138
147
|
/**
|
|
@@ -142,4 +151,4 @@ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success'
|
|
|
142
151
|
*/
|
|
143
152
|
declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
|
|
144
153
|
|
|
145
|
-
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, stepPad };
|
|
154
|
+
export { type ActivationTarget, DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, USER_ACTIVATION_EVENTS, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, stepPad };
|
package/dist/index.js
CHANGED
|
@@ -127,6 +127,7 @@ var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
|
127
127
|
var DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
128
128
|
var DISABLED_VALUE = "0";
|
|
129
129
|
var ENABLED_VALUE = "1";
|
|
130
|
+
var USER_ACTIVATION_EVENTS = ["pointerdown", "keydown", "touchstart"];
|
|
130
131
|
var HAPTIC_PATTERNS = {
|
|
131
132
|
/** Light tick for menu navigation / selection / undo / restart. */
|
|
132
133
|
tap: 10,
|
|
@@ -148,6 +149,29 @@ var HAPTIC_PATTERNS = {
|
|
|
148
149
|
reveal: [0, 14, 20, 18, 26, 24]
|
|
149
150
|
};
|
|
150
151
|
|
|
152
|
+
// src/userActivation.ts
|
|
153
|
+
function resolveActivationTarget(explicit) {
|
|
154
|
+
if (explicit) {
|
|
155
|
+
return explicit;
|
|
156
|
+
}
|
|
157
|
+
const candidate = globalThis;
|
|
158
|
+
return typeof candidate.addEventListener === "function" ? candidate : void 0;
|
|
159
|
+
}
|
|
160
|
+
function createActivationProbe(navigator, explicitTarget) {
|
|
161
|
+
let latched = false;
|
|
162
|
+
const target = typeof navigator?.vibrate === "function" ? resolveActivationTarget(explicitTarget) : void 0;
|
|
163
|
+
const onGesture = () => {
|
|
164
|
+
latched = true;
|
|
165
|
+
for (const type of USER_ACTIVATION_EVENTS) {
|
|
166
|
+
target?.removeEventListener(type, onGesture, true);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
for (const type of USER_ACTIVATION_EVENTS) {
|
|
170
|
+
target?.addEventListener(type, onGesture, true);
|
|
171
|
+
}
|
|
172
|
+
return () => latched || navigator?.userActivation?.hasBeenActive === true;
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
// src/haptics.ts
|
|
152
176
|
function resolveNavigator(explicit) {
|
|
153
177
|
if (explicit) {
|
|
@@ -166,6 +190,7 @@ function createHaptics(options = {}) {
|
|
|
166
190
|
const confirmation = options.enableConfirmation === void 0 ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;
|
|
167
191
|
const navigator = resolveNavigator(options.navigator);
|
|
168
192
|
const storage = resolveStorage(options.storage);
|
|
193
|
+
const hasUserActivation = createActivationProbe(navigator, options.activationTarget);
|
|
169
194
|
const readPref = () => {
|
|
170
195
|
try {
|
|
171
196
|
return storage?.getItem(storageKey) !== DISABLED_VALUE;
|
|
@@ -175,18 +200,20 @@ function createHaptics(options = {}) {
|
|
|
175
200
|
};
|
|
176
201
|
let enabled = readPref();
|
|
177
202
|
const vibrate = (pattern) => {
|
|
178
|
-
if (!enabled) {
|
|
203
|
+
if (!enabled || !hasUserActivation()) {
|
|
179
204
|
return;
|
|
180
205
|
}
|
|
181
206
|
try {
|
|
182
207
|
navigator?.vibrate?.(pattern);
|
|
183
208
|
} catch {
|
|
209
|
+
return;
|
|
184
210
|
}
|
|
185
211
|
};
|
|
186
212
|
const persist = () => {
|
|
187
213
|
try {
|
|
188
214
|
storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);
|
|
189
215
|
} catch {
|
|
216
|
+
return;
|
|
190
217
|
}
|
|
191
218
|
};
|
|
192
219
|
const setEnabled = (next) => {
|
|
@@ -211,6 +238,7 @@ exports.HAPTIC_PATTERNS = HAPTIC_PATTERNS;
|
|
|
211
238
|
exports.IDLE_REPEAT = IDLE_REPEAT;
|
|
212
239
|
exports.PAD_ACTIONS = PAD_ACTIONS;
|
|
213
240
|
exports.STANDARD_BUTTON = STANDARD_BUTTON;
|
|
241
|
+
exports.USER_ACTIVATION_EVENTS = USER_ACTIVATION_EVENTS;
|
|
214
242
|
exports.createGamepadPoller = createGamepadPoller;
|
|
215
243
|
exports.createHaptics = createHaptics;
|
|
216
244
|
exports.stepPad = stepPad;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/userActivation.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\r\n\r\n/** Default localStorage key for the haptics on/off preference. */\r\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\r\n\r\n/** Default confirmation tick fired when haptics are turned ON (ms). */\r\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\r\n\r\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\r\nexport const DISABLED_VALUE = '0';\r\n\r\n/** Stored value meaning \"enabled\". */\r\nexport const ENABLED_VALUE = '1';\r\n\r\n/** DOM events that count as a user activation before `navigator.userActivation` is available. */\r\nexport const USER_ACTIVATION_EVENTS = ['pointerdown', 'keydown', 'touchstart'] as const;\r\n\r\n/** The semantic intents games map their events onto. */\r\nexport type HapticIntent =\r\n | 'tap'\r\n | 'select'\r\n | 'lock'\r\n | 'bump'\r\n | 'navigate'\r\n | 'success'\r\n | 'error'\r\n | 'loss'\r\n | 'reveal';\r\n\r\n/**\r\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\r\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\r\n * {@link Haptics.vibrate} (or supply your own pattern).\r\n */\r\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\r\n /** Light tick for menu navigation / selection / undo / restart. */\r\n tap: 10,\r\n /** Light pulse when a tool/charm/item is picked up or selected. */\r\n select: 12,\r\n /** Short pulse when something locks into place. */\r\n lock: 18,\r\n /** Dull buzz when an action is blocked. */\r\n bump: 24,\r\n /** Quick pulse when stepping through a portal / page turn. */\r\n navigate: 16,\r\n /** Celebratory pattern when a level/chamber is cleared. */\r\n success: [0, 40, 50, 90],\r\n /** Sharp double crack for a fatal/destructive event. */\r\n error: [0, 14, 24, 14],\r\n /** Heavier buzz when the player loses / falls. */\r\n loss: [0, 40, 50, 40],\r\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\r\n reveal: [0, 14, 20, 18, 26, 24],\r\n};\r\n","import { USER_ACTIVATION_EVENTS } from './constants';\nimport type { ActivationTarget, VibrationCapable } from './types';\n\nfunction resolveActivationTarget(explicit?: ActivationTarget): ActivationTarget | undefined {\n if (explicit) {return explicit;}\n const candidate = globalThis as Partial<ActivationTarget>;\n return typeof candidate.addEventListener === 'function' ? (candidate as ActivationTarget) : undefined;\n}\n\n/** Returns a probe that is true once the page has received a user gesture. */\nexport function createActivationProbe(\n navigator: VibrationCapable | undefined,\n explicitTarget?: ActivationTarget,\n): () => boolean {\n let latched = false;\n const target = typeof navigator?.vibrate === 'function' ? resolveActivationTarget(explicitTarget) : undefined;\n\n const onGesture = (): void => {\n latched = true;\n for (const type of USER_ACTIVATION_EVENTS) {target?.removeEventListener(type, onGesture, true);}\n };\n\n for (const type of USER_ACTIVATION_EVENTS) {target?.addEventListener(type, onGesture, true);}\n\n return (): boolean => latched || navigator?.userActivation?.hasBeenActive === true;\n}\n","import {\r\n DEFAULT_ENABLE_CONFIRMATION,\r\n DEFAULT_HAPTICS_STORAGE_KEY,\r\n DISABLED_VALUE,\r\n ENABLED_VALUE,\r\n} from './constants';\r\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\r\nimport { createActivationProbe } from './userActivation';\r\n\r\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\r\n if (explicit) {\r\n return explicit;\r\n }\r\n return (globalThis as { navigator?: VibrationCapable }).navigator;\r\n}\r\n\r\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\r\n if (explicit) {\r\n return explicit;\r\n }\r\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\r\n}\r\n\r\n/**\r\n * Create a player-toggleable, always-safe haptics controller: it never vibrates before a\r\n * user gesture, while disabled, or when the Vibration API is missing or blocked.\r\n */\r\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\r\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\r\n const confirmation =\r\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\r\n const navigator = resolveNavigator(options.navigator);\r\n const storage = resolveStorage(options.storage);\r\n const hasUserActivation = createActivationProbe(navigator, options.activationTarget);\r\n\r\n const readPref = (): boolean => {\r\n try {\r\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\r\n } catch {\r\n return true;\r\n }\r\n };\r\n\r\n let enabled = readPref();\r\n\r\n const vibrate = (pattern: VibrationPattern): void => {\r\n if (!enabled || !hasUserActivation()) {\r\n return;\r\n }\r\n try {\r\n navigator?.vibrate?.(pattern);\r\n } catch {\r\n return;\r\n }\r\n };\r\n\r\n const persist = (): void => {\r\n try {\r\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\r\n } catch {\r\n return;\r\n }\r\n };\r\n\r\n const setEnabled = (next: boolean): boolean => {\r\n enabled = next;\r\n persist();\r\n if (enabled && confirmation !== null) {\r\n vibrate(confirmation);\r\n }\r\n return enabled;\r\n };\r\n\r\n return {\r\n isEnabled: (): boolean => enabled,\r\n setEnabled,\r\n toggle: (): boolean => setEnabled(!enabled),\r\n vibrate,\r\n };\r\n}\r\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -125,6 +125,7 @@ var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
|
125
125
|
var DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
126
126
|
var DISABLED_VALUE = "0";
|
|
127
127
|
var ENABLED_VALUE = "1";
|
|
128
|
+
var USER_ACTIVATION_EVENTS = ["pointerdown", "keydown", "touchstart"];
|
|
128
129
|
var HAPTIC_PATTERNS = {
|
|
129
130
|
/** Light tick for menu navigation / selection / undo / restart. */
|
|
130
131
|
tap: 10,
|
|
@@ -146,6 +147,29 @@ var HAPTIC_PATTERNS = {
|
|
|
146
147
|
reveal: [0, 14, 20, 18, 26, 24]
|
|
147
148
|
};
|
|
148
149
|
|
|
150
|
+
// src/userActivation.ts
|
|
151
|
+
function resolveActivationTarget(explicit) {
|
|
152
|
+
if (explicit) {
|
|
153
|
+
return explicit;
|
|
154
|
+
}
|
|
155
|
+
const candidate = globalThis;
|
|
156
|
+
return typeof candidate.addEventListener === "function" ? candidate : void 0;
|
|
157
|
+
}
|
|
158
|
+
function createActivationProbe(navigator, explicitTarget) {
|
|
159
|
+
let latched = false;
|
|
160
|
+
const target = typeof navigator?.vibrate === "function" ? resolveActivationTarget(explicitTarget) : void 0;
|
|
161
|
+
const onGesture = () => {
|
|
162
|
+
latched = true;
|
|
163
|
+
for (const type of USER_ACTIVATION_EVENTS) {
|
|
164
|
+
target?.removeEventListener(type, onGesture, true);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
for (const type of USER_ACTIVATION_EVENTS) {
|
|
168
|
+
target?.addEventListener(type, onGesture, true);
|
|
169
|
+
}
|
|
170
|
+
return () => latched || navigator?.userActivation?.hasBeenActive === true;
|
|
171
|
+
}
|
|
172
|
+
|
|
149
173
|
// src/haptics.ts
|
|
150
174
|
function resolveNavigator(explicit) {
|
|
151
175
|
if (explicit) {
|
|
@@ -164,6 +188,7 @@ function createHaptics(options = {}) {
|
|
|
164
188
|
const confirmation = options.enableConfirmation === void 0 ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;
|
|
165
189
|
const navigator = resolveNavigator(options.navigator);
|
|
166
190
|
const storage = resolveStorage(options.storage);
|
|
191
|
+
const hasUserActivation = createActivationProbe(navigator, options.activationTarget);
|
|
167
192
|
const readPref = () => {
|
|
168
193
|
try {
|
|
169
194
|
return storage?.getItem(storageKey) !== DISABLED_VALUE;
|
|
@@ -173,18 +198,20 @@ function createHaptics(options = {}) {
|
|
|
173
198
|
};
|
|
174
199
|
let enabled = readPref();
|
|
175
200
|
const vibrate = (pattern) => {
|
|
176
|
-
if (!enabled) {
|
|
201
|
+
if (!enabled || !hasUserActivation()) {
|
|
177
202
|
return;
|
|
178
203
|
}
|
|
179
204
|
try {
|
|
180
205
|
navigator?.vibrate?.(pattern);
|
|
181
206
|
} catch {
|
|
207
|
+
return;
|
|
182
208
|
}
|
|
183
209
|
};
|
|
184
210
|
const persist = () => {
|
|
185
211
|
try {
|
|
186
212
|
storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);
|
|
187
213
|
} catch {
|
|
214
|
+
return;
|
|
188
215
|
}
|
|
189
216
|
};
|
|
190
217
|
const setEnabled = (next) => {
|
|
@@ -203,6 +230,6 @@ function createHaptics(options = {}) {
|
|
|
203
230
|
};
|
|
204
231
|
}
|
|
205
232
|
|
|
206
|
-
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, IDLE_REPEAT, PAD_ACTIONS, STANDARD_BUTTON, createGamepadPoller, createHaptics, stepPad };
|
|
233
|
+
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, IDLE_REPEAT, PAD_ACTIONS, STANDARD_BUTTON, USER_ACTIVATION_EVENTS, createGamepadPoller, createHaptics, stepPad };
|
|
207
234
|
//# sourceMappingURL=index.mjs.map
|
|
208
235
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/userActivation.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\r\n\r\n/** Default localStorage key for the haptics on/off preference. */\r\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\r\n\r\n/** Default confirmation tick fired when haptics are turned ON (ms). */\r\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\r\n\r\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\r\nexport const DISABLED_VALUE = '0';\r\n\r\n/** Stored value meaning \"enabled\". */\r\nexport const ENABLED_VALUE = '1';\r\n\r\n/** DOM events that count as a user activation before `navigator.userActivation` is available. */\r\nexport const USER_ACTIVATION_EVENTS = ['pointerdown', 'keydown', 'touchstart'] as const;\r\n\r\n/** The semantic intents games map their events onto. */\r\nexport type HapticIntent =\r\n | 'tap'\r\n | 'select'\r\n | 'lock'\r\n | 'bump'\r\n | 'navigate'\r\n | 'success'\r\n | 'error'\r\n | 'loss'\r\n | 'reveal';\r\n\r\n/**\r\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\r\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\r\n * {@link Haptics.vibrate} (or supply your own pattern).\r\n */\r\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\r\n /** Light tick for menu navigation / selection / undo / restart. */\r\n tap: 10,\r\n /** Light pulse when a tool/charm/item is picked up or selected. */\r\n select: 12,\r\n /** Short pulse when something locks into place. */\r\n lock: 18,\r\n /** Dull buzz when an action is blocked. */\r\n bump: 24,\r\n /** Quick pulse when stepping through a portal / page turn. */\r\n navigate: 16,\r\n /** Celebratory pattern when a level/chamber is cleared. */\r\n success: [0, 40, 50, 90],\r\n /** Sharp double crack for a fatal/destructive event. */\r\n error: [0, 14, 24, 14],\r\n /** Heavier buzz when the player loses / falls. */\r\n loss: [0, 40, 50, 40],\r\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\r\n reveal: [0, 14, 20, 18, 26, 24],\r\n};\r\n","import { USER_ACTIVATION_EVENTS } from './constants';\nimport type { ActivationTarget, VibrationCapable } from './types';\n\nfunction resolveActivationTarget(explicit?: ActivationTarget): ActivationTarget | undefined {\n if (explicit) {return explicit;}\n const candidate = globalThis as Partial<ActivationTarget>;\n return typeof candidate.addEventListener === 'function' ? (candidate as ActivationTarget) : undefined;\n}\n\n/** Returns a probe that is true once the page has received a user gesture. */\nexport function createActivationProbe(\n navigator: VibrationCapable | undefined,\n explicitTarget?: ActivationTarget,\n): () => boolean {\n let latched = false;\n const target = typeof navigator?.vibrate === 'function' ? resolveActivationTarget(explicitTarget) : undefined;\n\n const onGesture = (): void => {\n latched = true;\n for (const type of USER_ACTIVATION_EVENTS) {target?.removeEventListener(type, onGesture, true);}\n };\n\n for (const type of USER_ACTIVATION_EVENTS) {target?.addEventListener(type, onGesture, true);}\n\n return (): boolean => latched || navigator?.userActivation?.hasBeenActive === true;\n}\n","import {\r\n DEFAULT_ENABLE_CONFIRMATION,\r\n DEFAULT_HAPTICS_STORAGE_KEY,\r\n DISABLED_VALUE,\r\n ENABLED_VALUE,\r\n} from './constants';\r\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\r\nimport { createActivationProbe } from './userActivation';\r\n\r\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\r\n if (explicit) {\r\n return explicit;\r\n }\r\n return (globalThis as { navigator?: VibrationCapable }).navigator;\r\n}\r\n\r\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\r\n if (explicit) {\r\n return explicit;\r\n }\r\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\r\n}\r\n\r\n/**\r\n * Create a player-toggleable, always-safe haptics controller: it never vibrates before a\r\n * user gesture, while disabled, or when the Vibration API is missing or blocked.\r\n */\r\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\r\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\r\n const confirmation =\r\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\r\n const navigator = resolveNavigator(options.navigator);\r\n const storage = resolveStorage(options.storage);\r\n const hasUserActivation = createActivationProbe(navigator, options.activationTarget);\r\n\r\n const readPref = (): boolean => {\r\n try {\r\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\r\n } catch {\r\n return true;\r\n }\r\n };\r\n\r\n let enabled = readPref();\r\n\r\n const vibrate = (pattern: VibrationPattern): void => {\r\n if (!enabled || !hasUserActivation()) {\r\n return;\r\n }\r\n try {\r\n navigator?.vibrate?.(pattern);\r\n } catch {\r\n return;\r\n }\r\n };\r\n\r\n const persist = (): void => {\r\n try {\r\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\r\n } catch {\r\n return;\r\n }\r\n };\r\n\r\n const setEnabled = (next: boolean): boolean => {\r\n enabled = next;\r\n persist();\r\n if (enabled && confirmation !== null) {\r\n vibrate(confirmation);\r\n }\r\n return enabled;\r\n };\r\n\r\n return {\r\n isEnabled: (): boolean => enabled,\r\n setEnabled,\r\n toggle: (): boolean => setEnabled(!enabled),\r\n vibrate,\r\n };\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dloizides/game-input",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Framework-agnostic game input primitives. v1 ships safe, player-toggleable haptics (Vibration API wrapper, localStorage-gated, zero deps). Extracted from Aurora; consumed across EisaiPollis games.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"haptics",
|