@hapticjs/gamepad 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +164 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/gamepad-manager.ts
|
|
4
|
+
var GamepadManager = class {
|
|
5
|
+
constructor() {
|
|
6
|
+
this._listening = false;
|
|
7
|
+
this._handleConnect = (e) => {
|
|
8
|
+
this._onConnect?.(e.gamepad);
|
|
9
|
+
};
|
|
10
|
+
this._handleDisconnect = (e) => {
|
|
11
|
+
this._onDisconnect?.(e.gamepad);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Start listening for gamepad connections */
|
|
15
|
+
listen() {
|
|
16
|
+
if (this._listening || typeof window === "undefined") return;
|
|
17
|
+
this._listening = true;
|
|
18
|
+
window.addEventListener("gamepadconnected", this._handleConnect);
|
|
19
|
+
window.addEventListener("gamepaddisconnected", this._handleDisconnect);
|
|
20
|
+
}
|
|
21
|
+
/** Stop listening */
|
|
22
|
+
unlisten() {
|
|
23
|
+
if (!this._listening || typeof window === "undefined") return;
|
|
24
|
+
this._listening = false;
|
|
25
|
+
window.removeEventListener("gamepadconnected", this._handleConnect);
|
|
26
|
+
window.removeEventListener("gamepaddisconnected", this._handleDisconnect);
|
|
27
|
+
}
|
|
28
|
+
/** Get all currently connected gamepads */
|
|
29
|
+
getGamepads() {
|
|
30
|
+
if (typeof navigator === "undefined" || !("getGamepads" in navigator)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return Array.from(navigator.getGamepads()).filter(
|
|
34
|
+
(gp) => gp !== null
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
/** Get the first connected gamepad, or null */
|
|
38
|
+
getFirstGamepad() {
|
|
39
|
+
const gamepads = this.getGamepads();
|
|
40
|
+
return gamepads[0] ?? null;
|
|
41
|
+
}
|
|
42
|
+
/** Set connection callback */
|
|
43
|
+
onConnect(fn) {
|
|
44
|
+
this._onConnect = fn;
|
|
45
|
+
}
|
|
46
|
+
/** Set disconnection callback */
|
|
47
|
+
onDisconnect(fn) {
|
|
48
|
+
this._onDisconnect = fn;
|
|
49
|
+
}
|
|
50
|
+
/** Check if any gamepad with haptic support is connected */
|
|
51
|
+
hasHapticGamepad() {
|
|
52
|
+
return this.getGamepads().some(
|
|
53
|
+
(gp) => gp.vibrationActuator != null
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
dispose() {
|
|
57
|
+
this.unlisten();
|
|
58
|
+
this._onConnect = void 0;
|
|
59
|
+
this._onDisconnect = void 0;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/utils/dual-motor.ts
|
|
64
|
+
var defaultMotorMapping = (intensity) => {
|
|
65
|
+
return {
|
|
66
|
+
weakMagnitude: Math.min(intensity * 1.5, 1),
|
|
67
|
+
strongMagnitude: Math.max(intensity - 0.3, 0)
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
var equalMotorMapping = (intensity) => {
|
|
71
|
+
return {
|
|
72
|
+
weakMagnitude: intensity,
|
|
73
|
+
strongMagnitude: intensity
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
var heavyMotorMapping = (intensity) => {
|
|
77
|
+
return {
|
|
78
|
+
weakMagnitude: intensity * 0.3,
|
|
79
|
+
strongMagnitude: intensity
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/adapters/gamepad-haptic.adapter.ts
|
|
84
|
+
var GamepadHapticAdapter = class {
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
this.name = "gamepad";
|
|
87
|
+
this._cancelled = false;
|
|
88
|
+
this.manager = new GamepadManager();
|
|
89
|
+
this.motorMapping = options.motorMapping ?? defaultMotorMapping;
|
|
90
|
+
this.gamepadIndex = options.gamepadIndex;
|
|
91
|
+
if (options.autoListen !== false) {
|
|
92
|
+
this.manager.listen();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
get supported() {
|
|
96
|
+
return this.manager.hasHapticGamepad();
|
|
97
|
+
}
|
|
98
|
+
capabilities() {
|
|
99
|
+
return {
|
|
100
|
+
maxIntensityLevels: 100,
|
|
101
|
+
minDuration: 1,
|
|
102
|
+
maxDuration: 5e3,
|
|
103
|
+
supportsPattern: false,
|
|
104
|
+
supportsIntensity: true,
|
|
105
|
+
dualMotor: true
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async pulse(intensity, duration) {
|
|
109
|
+
const gamepad = this._getGamepad();
|
|
110
|
+
if (!gamepad?.vibrationActuator) return;
|
|
111
|
+
const { weakMagnitude, strongMagnitude } = this.motorMapping(intensity);
|
|
112
|
+
await gamepad.vibrationActuator.playEffect("dual-rumble", {
|
|
113
|
+
startDelay: 0,
|
|
114
|
+
duration,
|
|
115
|
+
weakMagnitude,
|
|
116
|
+
strongMagnitude
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async playSequence(steps) {
|
|
120
|
+
this._cancelled = false;
|
|
121
|
+
for (const step of steps) {
|
|
122
|
+
if (this._cancelled) break;
|
|
123
|
+
if (step.type === "vibrate" && step.intensity > 0) {
|
|
124
|
+
await this.pulse(step.intensity, step.duration);
|
|
125
|
+
await this._delay(step.duration);
|
|
126
|
+
} else {
|
|
127
|
+
await this._delay(step.duration);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
cancel() {
|
|
132
|
+
this._cancelled = true;
|
|
133
|
+
const gamepad = this._getGamepad();
|
|
134
|
+
if (gamepad?.vibrationActuator) {
|
|
135
|
+
gamepad.vibrationActuator.reset();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
dispose() {
|
|
139
|
+
this.cancel();
|
|
140
|
+
this.manager.dispose();
|
|
141
|
+
}
|
|
142
|
+
/** Get the gamepad manager for advanced usage */
|
|
143
|
+
getManager() {
|
|
144
|
+
return this.manager;
|
|
145
|
+
}
|
|
146
|
+
_getGamepad() {
|
|
147
|
+
if (this.gamepadIndex !== void 0) {
|
|
148
|
+
const gamepads = this.manager.getGamepads();
|
|
149
|
+
return gamepads.find((gp) => gp.index === this.gamepadIndex) ?? null;
|
|
150
|
+
}
|
|
151
|
+
return this.manager.getFirstGamepad();
|
|
152
|
+
}
|
|
153
|
+
_delay(ms) {
|
|
154
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
exports.GamepadHapticAdapter = GamepadHapticAdapter;
|
|
159
|
+
exports.GamepadManager = GamepadManager;
|
|
160
|
+
exports.defaultMotorMapping = defaultMotorMapping;
|
|
161
|
+
exports.equalMotorMapping = equalMotorMapping;
|
|
162
|
+
exports.heavyMotorMapping = heavyMotorMapping;
|
|
163
|
+
//# sourceMappingURL=index.cjs.map
|
|
164
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/gamepad-manager.ts","../src/utils/dual-motor.ts","../src/adapters/gamepad-haptic.adapter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** Manages gamepad connection state and provides access to connected gamepads */\nexport class GamepadManager {\n private _onConnect?: (gamepad: Gamepad) => void;\n private _onDisconnect?: (gamepad: Gamepad) => void;\n private _listening = false;\n\n /** Start listening for gamepad connections */\n listen(): void {\n if (this._listening || typeof window === 'undefined') return;\n this._listening = true;\n\n window.addEventListener('gamepadconnected', this._handleConnect);\n window.addEventListener('gamepaddisconnected', this._handleDisconnect);\n }\n\n /** Stop listening */\n unlisten(): void {\n if (!this._listening || typeof window === 'undefined') return;\n this._listening = false;\n\n window.removeEventListener('gamepadconnected', this._handleConnect);\n window.removeEventListener('gamepaddisconnected', this._handleDisconnect);\n }\n\n /** Get all currently connected gamepads */\n getGamepads(): Gamepad[] {\n if (typeof navigator === 'undefined' || !('getGamepads' in navigator)) {\n return [];\n }\n return Array.from(navigator.getGamepads()).filter(\n (gp): gp is Gamepad => gp !== null\n );\n }\n\n /** Get the first connected gamepad, or null */\n getFirstGamepad(): Gamepad | null {\n const gamepads = this.getGamepads();\n return gamepads[0] ?? null;\n }\n\n /** Set connection callback */\n onConnect(fn: (gamepad: Gamepad) => void): void {\n this._onConnect = fn;\n }\n\n /** Set disconnection callback */\n onDisconnect(fn: (gamepad: Gamepad) => void): void {\n this._onDisconnect = fn;\n }\n\n /** Check if any gamepad with haptic support is connected */\n hasHapticGamepad(): boolean {\n return this.getGamepads().some(\n (gp) => gp.vibrationActuator != null\n );\n }\n\n dispose(): void {\n this.unlisten();\n this._onConnect = undefined;\n this._onDisconnect = undefined;\n }\n\n private _handleConnect = (e: GamepadEvent) => {\n this._onConnect?.(e.gamepad);\n };\n\n private _handleDisconnect = (e: GamepadEvent) => {\n this._onDisconnect?.(e.gamepad);\n };\n}\n","/** Dual motor parameters for gamepad haptics */\nexport interface DualMotorParams {\n /** Weak (high-frequency) motor magnitude 0-1 */\n weakMagnitude: number;\n /** Strong (low-frequency) motor magnitude 0-1 */\n strongMagnitude: number;\n}\n\nexport type MotorMappingFn = (intensity: number) => DualMotorParams;\n\n/**\n * Default motor mapping: light effects use weak motor, heavy use strong, medium uses both.\n */\nexport const defaultMotorMapping: MotorMappingFn = (intensity: number): DualMotorParams => {\n return {\n weakMagnitude: Math.min(intensity * 1.5, 1.0),\n strongMagnitude: Math.max(intensity - 0.3, 0),\n };\n};\n\n/**\n * Equal mapping: both motors at same intensity.\n */\nexport const equalMotorMapping: MotorMappingFn = (intensity: number): DualMotorParams => {\n return {\n weakMagnitude: intensity,\n strongMagnitude: intensity,\n };\n};\n\n/**\n * Heavy mapping: emphasizes the strong (low-frequency) motor.\n */\nexport const heavyMotorMapping: MotorMappingFn = (intensity: number): DualMotorParams => {\n return {\n weakMagnitude: intensity * 0.3,\n strongMagnitude: intensity,\n };\n};\n","import type { HapticAdapter, AdapterCapabilities, HapticStep } from '@hapticjs/core';\nimport { GamepadManager } from '../utils/gamepad-manager';\nimport { defaultMotorMapping } from '../utils/dual-motor';\nimport type { MotorMappingFn } from '../utils/dual-motor';\n\nexport interface GamepadAdapterOptions {\n /** Which gamepad index to use (default: first connected) */\n gamepadIndex?: number;\n /** Custom motor mapping function */\n motorMapping?: MotorMappingFn;\n /** Auto-listen for gamepad connections (default: true) */\n autoListen?: boolean;\n}\n\n/**\n * Gamepad haptics adapter using the GamepadHapticActuator API.\n * Supports dual-motor (weak/strong) vibration on modern controllers.\n */\nexport class GamepadHapticAdapter implements HapticAdapter {\n readonly name = 'gamepad';\n private manager: GamepadManager;\n private motorMapping: MotorMappingFn;\n private gamepadIndex?: number;\n private _cancelled = false;\n\n constructor(options: GamepadAdapterOptions = {}) {\n this.manager = new GamepadManager();\n this.motorMapping = options.motorMapping ?? defaultMotorMapping;\n this.gamepadIndex = options.gamepadIndex;\n\n if (options.autoListen !== false) {\n this.manager.listen();\n }\n }\n\n get supported(): boolean {\n return this.manager.hasHapticGamepad();\n }\n\n capabilities(): AdapterCapabilities {\n return {\n maxIntensityLevels: 100,\n minDuration: 1,\n maxDuration: 5000,\n supportsPattern: false,\n supportsIntensity: true,\n dualMotor: true,\n };\n }\n\n async pulse(intensity: number, duration: number): Promise<void> {\n const gamepad = this._getGamepad();\n if (!gamepad?.vibrationActuator) return;\n\n const { weakMagnitude, strongMagnitude } = this.motorMapping(intensity);\n\n await gamepad.vibrationActuator.playEffect('dual-rumble', {\n startDelay: 0,\n duration,\n weakMagnitude,\n strongMagnitude,\n });\n }\n\n async playSequence(steps: HapticStep[]): Promise<void> {\n this._cancelled = false;\n\n for (const step of steps) {\n if (this._cancelled) break;\n\n if (step.type === 'vibrate' && step.intensity > 0) {\n await this.pulse(step.intensity, step.duration);\n await this._delay(step.duration);\n } else {\n await this._delay(step.duration);\n }\n }\n }\n\n cancel(): void {\n this._cancelled = true;\n const gamepad = this._getGamepad();\n if (gamepad?.vibrationActuator) {\n gamepad.vibrationActuator.reset();\n }\n }\n\n dispose(): void {\n this.cancel();\n this.manager.dispose();\n }\n\n /** Get the gamepad manager for advanced usage */\n getManager(): GamepadManager {\n return this.manager;\n }\n\n private _getGamepad(): Gamepad | null {\n if (this.gamepadIndex !== undefined) {\n const gamepads = this.manager.getGamepads();\n return gamepads.find((gp) => gp.index === this.gamepadIndex) ?? null;\n }\n return this.manager.getFirstGamepad();\n }\n\n private _delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HapticAdapter, AdapterCapabilities, HapticStep } from '@hapticjs/core';
|
|
2
|
+
|
|
3
|
+
/** Manages gamepad connection state and provides access to connected gamepads */
|
|
4
|
+
declare class GamepadManager {
|
|
5
|
+
private _onConnect?;
|
|
6
|
+
private _onDisconnect?;
|
|
7
|
+
private _listening;
|
|
8
|
+
/** Start listening for gamepad connections */
|
|
9
|
+
listen(): void;
|
|
10
|
+
/** Stop listening */
|
|
11
|
+
unlisten(): void;
|
|
12
|
+
/** Get all currently connected gamepads */
|
|
13
|
+
getGamepads(): Gamepad[];
|
|
14
|
+
/** Get the first connected gamepad, or null */
|
|
15
|
+
getFirstGamepad(): Gamepad | null;
|
|
16
|
+
/** Set connection callback */
|
|
17
|
+
onConnect(fn: (gamepad: Gamepad) => void): void;
|
|
18
|
+
/** Set disconnection callback */
|
|
19
|
+
onDisconnect(fn: (gamepad: Gamepad) => void): void;
|
|
20
|
+
/** Check if any gamepad with haptic support is connected */
|
|
21
|
+
hasHapticGamepad(): boolean;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
private _handleConnect;
|
|
24
|
+
private _handleDisconnect;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Dual motor parameters for gamepad haptics */
|
|
28
|
+
interface DualMotorParams {
|
|
29
|
+
/** Weak (high-frequency) motor magnitude 0-1 */
|
|
30
|
+
weakMagnitude: number;
|
|
31
|
+
/** Strong (low-frequency) motor magnitude 0-1 */
|
|
32
|
+
strongMagnitude: number;
|
|
33
|
+
}
|
|
34
|
+
type MotorMappingFn = (intensity: number) => DualMotorParams;
|
|
35
|
+
/**
|
|
36
|
+
* Default motor mapping: light effects use weak motor, heavy use strong, medium uses both.
|
|
37
|
+
*/
|
|
38
|
+
declare const defaultMotorMapping: MotorMappingFn;
|
|
39
|
+
/**
|
|
40
|
+
* Equal mapping: both motors at same intensity.
|
|
41
|
+
*/
|
|
42
|
+
declare const equalMotorMapping: MotorMappingFn;
|
|
43
|
+
/**
|
|
44
|
+
* Heavy mapping: emphasizes the strong (low-frequency) motor.
|
|
45
|
+
*/
|
|
46
|
+
declare const heavyMotorMapping: MotorMappingFn;
|
|
47
|
+
|
|
48
|
+
interface GamepadAdapterOptions {
|
|
49
|
+
/** Which gamepad index to use (default: first connected) */
|
|
50
|
+
gamepadIndex?: number;
|
|
51
|
+
/** Custom motor mapping function */
|
|
52
|
+
motorMapping?: MotorMappingFn;
|
|
53
|
+
/** Auto-listen for gamepad connections (default: true) */
|
|
54
|
+
autoListen?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Gamepad haptics adapter using the GamepadHapticActuator API.
|
|
58
|
+
* Supports dual-motor (weak/strong) vibration on modern controllers.
|
|
59
|
+
*/
|
|
60
|
+
declare class GamepadHapticAdapter implements HapticAdapter {
|
|
61
|
+
readonly name = "gamepad";
|
|
62
|
+
private manager;
|
|
63
|
+
private motorMapping;
|
|
64
|
+
private gamepadIndex?;
|
|
65
|
+
private _cancelled;
|
|
66
|
+
constructor(options?: GamepadAdapterOptions);
|
|
67
|
+
get supported(): boolean;
|
|
68
|
+
capabilities(): AdapterCapabilities;
|
|
69
|
+
pulse(intensity: number, duration: number): Promise<void>;
|
|
70
|
+
playSequence(steps: HapticStep[]): Promise<void>;
|
|
71
|
+
cancel(): void;
|
|
72
|
+
dispose(): void;
|
|
73
|
+
/** Get the gamepad manager for advanced usage */
|
|
74
|
+
getManager(): GamepadManager;
|
|
75
|
+
private _getGamepad;
|
|
76
|
+
private _delay;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { type DualMotorParams, type GamepadAdapterOptions, GamepadHapticAdapter, GamepadManager, type MotorMappingFn, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HapticAdapter, AdapterCapabilities, HapticStep } from '@hapticjs/core';
|
|
2
|
+
|
|
3
|
+
/** Manages gamepad connection state and provides access to connected gamepads */
|
|
4
|
+
declare class GamepadManager {
|
|
5
|
+
private _onConnect?;
|
|
6
|
+
private _onDisconnect?;
|
|
7
|
+
private _listening;
|
|
8
|
+
/** Start listening for gamepad connections */
|
|
9
|
+
listen(): void;
|
|
10
|
+
/** Stop listening */
|
|
11
|
+
unlisten(): void;
|
|
12
|
+
/** Get all currently connected gamepads */
|
|
13
|
+
getGamepads(): Gamepad[];
|
|
14
|
+
/** Get the first connected gamepad, or null */
|
|
15
|
+
getFirstGamepad(): Gamepad | null;
|
|
16
|
+
/** Set connection callback */
|
|
17
|
+
onConnect(fn: (gamepad: Gamepad) => void): void;
|
|
18
|
+
/** Set disconnection callback */
|
|
19
|
+
onDisconnect(fn: (gamepad: Gamepad) => void): void;
|
|
20
|
+
/** Check if any gamepad with haptic support is connected */
|
|
21
|
+
hasHapticGamepad(): boolean;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
private _handleConnect;
|
|
24
|
+
private _handleDisconnect;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Dual motor parameters for gamepad haptics */
|
|
28
|
+
interface DualMotorParams {
|
|
29
|
+
/** Weak (high-frequency) motor magnitude 0-1 */
|
|
30
|
+
weakMagnitude: number;
|
|
31
|
+
/** Strong (low-frequency) motor magnitude 0-1 */
|
|
32
|
+
strongMagnitude: number;
|
|
33
|
+
}
|
|
34
|
+
type MotorMappingFn = (intensity: number) => DualMotorParams;
|
|
35
|
+
/**
|
|
36
|
+
* Default motor mapping: light effects use weak motor, heavy use strong, medium uses both.
|
|
37
|
+
*/
|
|
38
|
+
declare const defaultMotorMapping: MotorMappingFn;
|
|
39
|
+
/**
|
|
40
|
+
* Equal mapping: both motors at same intensity.
|
|
41
|
+
*/
|
|
42
|
+
declare const equalMotorMapping: MotorMappingFn;
|
|
43
|
+
/**
|
|
44
|
+
* Heavy mapping: emphasizes the strong (low-frequency) motor.
|
|
45
|
+
*/
|
|
46
|
+
declare const heavyMotorMapping: MotorMappingFn;
|
|
47
|
+
|
|
48
|
+
interface GamepadAdapterOptions {
|
|
49
|
+
/** Which gamepad index to use (default: first connected) */
|
|
50
|
+
gamepadIndex?: number;
|
|
51
|
+
/** Custom motor mapping function */
|
|
52
|
+
motorMapping?: MotorMappingFn;
|
|
53
|
+
/** Auto-listen for gamepad connections (default: true) */
|
|
54
|
+
autoListen?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Gamepad haptics adapter using the GamepadHapticActuator API.
|
|
58
|
+
* Supports dual-motor (weak/strong) vibration on modern controllers.
|
|
59
|
+
*/
|
|
60
|
+
declare class GamepadHapticAdapter implements HapticAdapter {
|
|
61
|
+
readonly name = "gamepad";
|
|
62
|
+
private manager;
|
|
63
|
+
private motorMapping;
|
|
64
|
+
private gamepadIndex?;
|
|
65
|
+
private _cancelled;
|
|
66
|
+
constructor(options?: GamepadAdapterOptions);
|
|
67
|
+
get supported(): boolean;
|
|
68
|
+
capabilities(): AdapterCapabilities;
|
|
69
|
+
pulse(intensity: number, duration: number): Promise<void>;
|
|
70
|
+
playSequence(steps: HapticStep[]): Promise<void>;
|
|
71
|
+
cancel(): void;
|
|
72
|
+
dispose(): void;
|
|
73
|
+
/** Get the gamepad manager for advanced usage */
|
|
74
|
+
getManager(): GamepadManager;
|
|
75
|
+
private _getGamepad;
|
|
76
|
+
private _delay;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { type DualMotorParams, type GamepadAdapterOptions, GamepadHapticAdapter, GamepadManager, type MotorMappingFn, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/utils/gamepad-manager.ts
|
|
2
|
+
var GamepadManager = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this._listening = false;
|
|
5
|
+
this._handleConnect = (e) => {
|
|
6
|
+
this._onConnect?.(e.gamepad);
|
|
7
|
+
};
|
|
8
|
+
this._handleDisconnect = (e) => {
|
|
9
|
+
this._onDisconnect?.(e.gamepad);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Start listening for gamepad connections */
|
|
13
|
+
listen() {
|
|
14
|
+
if (this._listening || typeof window === "undefined") return;
|
|
15
|
+
this._listening = true;
|
|
16
|
+
window.addEventListener("gamepadconnected", this._handleConnect);
|
|
17
|
+
window.addEventListener("gamepaddisconnected", this._handleDisconnect);
|
|
18
|
+
}
|
|
19
|
+
/** Stop listening */
|
|
20
|
+
unlisten() {
|
|
21
|
+
if (!this._listening || typeof window === "undefined") return;
|
|
22
|
+
this._listening = false;
|
|
23
|
+
window.removeEventListener("gamepadconnected", this._handleConnect);
|
|
24
|
+
window.removeEventListener("gamepaddisconnected", this._handleDisconnect);
|
|
25
|
+
}
|
|
26
|
+
/** Get all currently connected gamepads */
|
|
27
|
+
getGamepads() {
|
|
28
|
+
if (typeof navigator === "undefined" || !("getGamepads" in navigator)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return Array.from(navigator.getGamepads()).filter(
|
|
32
|
+
(gp) => gp !== null
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
/** Get the first connected gamepad, or null */
|
|
36
|
+
getFirstGamepad() {
|
|
37
|
+
const gamepads = this.getGamepads();
|
|
38
|
+
return gamepads[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
/** Set connection callback */
|
|
41
|
+
onConnect(fn) {
|
|
42
|
+
this._onConnect = fn;
|
|
43
|
+
}
|
|
44
|
+
/** Set disconnection callback */
|
|
45
|
+
onDisconnect(fn) {
|
|
46
|
+
this._onDisconnect = fn;
|
|
47
|
+
}
|
|
48
|
+
/** Check if any gamepad with haptic support is connected */
|
|
49
|
+
hasHapticGamepad() {
|
|
50
|
+
return this.getGamepads().some(
|
|
51
|
+
(gp) => gp.vibrationActuator != null
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
dispose() {
|
|
55
|
+
this.unlisten();
|
|
56
|
+
this._onConnect = void 0;
|
|
57
|
+
this._onDisconnect = void 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/utils/dual-motor.ts
|
|
62
|
+
var defaultMotorMapping = (intensity) => {
|
|
63
|
+
return {
|
|
64
|
+
weakMagnitude: Math.min(intensity * 1.5, 1),
|
|
65
|
+
strongMagnitude: Math.max(intensity - 0.3, 0)
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
var equalMotorMapping = (intensity) => {
|
|
69
|
+
return {
|
|
70
|
+
weakMagnitude: intensity,
|
|
71
|
+
strongMagnitude: intensity
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
var heavyMotorMapping = (intensity) => {
|
|
75
|
+
return {
|
|
76
|
+
weakMagnitude: intensity * 0.3,
|
|
77
|
+
strongMagnitude: intensity
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/adapters/gamepad-haptic.adapter.ts
|
|
82
|
+
var GamepadHapticAdapter = class {
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.name = "gamepad";
|
|
85
|
+
this._cancelled = false;
|
|
86
|
+
this.manager = new GamepadManager();
|
|
87
|
+
this.motorMapping = options.motorMapping ?? defaultMotorMapping;
|
|
88
|
+
this.gamepadIndex = options.gamepadIndex;
|
|
89
|
+
if (options.autoListen !== false) {
|
|
90
|
+
this.manager.listen();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
get supported() {
|
|
94
|
+
return this.manager.hasHapticGamepad();
|
|
95
|
+
}
|
|
96
|
+
capabilities() {
|
|
97
|
+
return {
|
|
98
|
+
maxIntensityLevels: 100,
|
|
99
|
+
minDuration: 1,
|
|
100
|
+
maxDuration: 5e3,
|
|
101
|
+
supportsPattern: false,
|
|
102
|
+
supportsIntensity: true,
|
|
103
|
+
dualMotor: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async pulse(intensity, duration) {
|
|
107
|
+
const gamepad = this._getGamepad();
|
|
108
|
+
if (!gamepad?.vibrationActuator) return;
|
|
109
|
+
const { weakMagnitude, strongMagnitude } = this.motorMapping(intensity);
|
|
110
|
+
await gamepad.vibrationActuator.playEffect("dual-rumble", {
|
|
111
|
+
startDelay: 0,
|
|
112
|
+
duration,
|
|
113
|
+
weakMagnitude,
|
|
114
|
+
strongMagnitude
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async playSequence(steps) {
|
|
118
|
+
this._cancelled = false;
|
|
119
|
+
for (const step of steps) {
|
|
120
|
+
if (this._cancelled) break;
|
|
121
|
+
if (step.type === "vibrate" && step.intensity > 0) {
|
|
122
|
+
await this.pulse(step.intensity, step.duration);
|
|
123
|
+
await this._delay(step.duration);
|
|
124
|
+
} else {
|
|
125
|
+
await this._delay(step.duration);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
cancel() {
|
|
130
|
+
this._cancelled = true;
|
|
131
|
+
const gamepad = this._getGamepad();
|
|
132
|
+
if (gamepad?.vibrationActuator) {
|
|
133
|
+
gamepad.vibrationActuator.reset();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
dispose() {
|
|
137
|
+
this.cancel();
|
|
138
|
+
this.manager.dispose();
|
|
139
|
+
}
|
|
140
|
+
/** Get the gamepad manager for advanced usage */
|
|
141
|
+
getManager() {
|
|
142
|
+
return this.manager;
|
|
143
|
+
}
|
|
144
|
+
_getGamepad() {
|
|
145
|
+
if (this.gamepadIndex !== void 0) {
|
|
146
|
+
const gamepads = this.manager.getGamepads();
|
|
147
|
+
return gamepads.find((gp) => gp.index === this.gamepadIndex) ?? null;
|
|
148
|
+
}
|
|
149
|
+
return this.manager.getFirstGamepad();
|
|
150
|
+
}
|
|
151
|
+
_delay(ms) {
|
|
152
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export { GamepadHapticAdapter, GamepadManager, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/gamepad-manager.ts","../src/utils/dual-motor.ts","../src/adapters/gamepad-haptic.adapter.ts"],"names":[],"mappings":";AACO,IAAM,iBAAN,MAAqB;AAAA,EAArB,WAAA,GAAA;AAGL,IAAA,IAAA,CAAQ,UAAA,GAAa,KAAA;AA2DrB,IAAA,IAAA,CAAQ,cAAA,GAAiB,CAAC,CAAA,KAAoB;AAC5C,MAAA,IAAA,CAAK,UAAA,GAAa,EAAE,OAAO,CAAA;AAAA,IAC7B,CAAA;AAEA,IAAA,IAAA,CAAQ,iBAAA,GAAoB,CAAC,CAAA,KAAoB;AAC/C,MAAA,IAAA,CAAK,aAAA,GAAgB,EAAE,OAAO,CAAA;AAAA,IAChC,CAAA;AAAA,EAAA;AAAA;AAAA,EA9DA,MAAA,GAAe;AACb,IAAA,IAAI,IAAA,CAAK,UAAA,IAAc,OAAO,MAAA,KAAW,WAAA,EAAa;AACtD,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAElB,IAAA,MAAA,CAAO,gBAAA,CAAiB,kBAAA,EAAoB,IAAA,CAAK,cAAc,CAAA;AAC/D,IAAA,MAAA,CAAO,gBAAA,CAAiB,qBAAA,EAAuB,IAAA,CAAK,iBAAiB,CAAA;AAAA,EACvE;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,UAAA,IAAc,OAAO,WAAW,WAAA,EAAa;AACvD,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAElB,IAAA,MAAA,CAAO,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,cAAc,CAAA;AAClE,IAAA,MAAA,CAAO,mBAAA,CAAoB,qBAAA,EAAuB,IAAA,CAAK,iBAAiB,CAAA;AAAA,EAC1E;AAAA;AAAA,EAGA,WAAA,GAAyB;AACvB,IAAA,IAAI,OAAO,SAAA,KAAc,WAAA,IAAe,EAAE,iBAAiB,SAAA,CAAA,EAAY;AACrE,MAAA,OAAO,EAAC;AAAA,IACV;AACA,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,SAAA,CAAU,WAAA,EAAa,CAAA,CAAE,MAAA;AAAA,MACzC,CAAC,OAAsB,EAAA,KAAO;AAAA,KAChC;AAAA,EACF;AAAA;AAAA,EAGA,eAAA,GAAkC;AAChC,IAAA,MAAM,QAAA,GAAW,KAAK,WAAA,EAAY;AAClC,IAAA,OAAO,QAAA,CAAS,CAAC,CAAA,IAAK,IAAA;AAAA,EACxB;AAAA;AAAA,EAGA,UAAU,EAAA,EAAsC;AAC9C,IAAA,IAAA,CAAK,UAAA,GAAa,EAAA;AAAA,EACpB;AAAA;AAAA,EAGA,aAAa,EAAA,EAAsC;AACjD,IAAA,IAAA,CAAK,aAAA,GAAgB,EAAA;AAAA,EACvB;AAAA;AAAA,EAGA,gBAAA,GAA4B;AAC1B,IAAA,OAAO,IAAA,CAAK,aAAY,CAAE,IAAA;AAAA,MACxB,CAAC,EAAA,KAAO,EAAA,CAAG,iBAAA,IAAqB;AAAA,KAClC;AAAA,EACF;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,QAAA,EAAS;AACd,IAAA,IAAA,CAAK,UAAA,GAAa,MAAA;AAClB,IAAA,IAAA,CAAK,aAAA,GAAgB,MAAA;AAAA,EACvB;AASF;;;ACzDO,IAAM,mBAAA,GAAsC,CAAC,SAAA,KAAuC;AACzF,EAAA,OAAO;AAAA,IACL,aAAA,EAAe,IAAA,CAAK,GAAA,CAAI,SAAA,GAAY,KAAK,CAAG,CAAA;AAAA,IAC5C,eAAA,EAAiB,IAAA,CAAK,GAAA,CAAI,SAAA,GAAY,KAAK,CAAC;AAAA,GAC9C;AACF;AAKO,IAAM,iBAAA,GAAoC,CAAC,SAAA,KAAuC;AACvF,EAAA,OAAO;AAAA,IACL,aAAA,EAAe,SAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACnB;AACF;AAKO,IAAM,iBAAA,GAAoC,CAAC,SAAA,KAAuC;AACvF,EAAA,OAAO;AAAA,IACL,eAAe,SAAA,GAAY,GAAA;AAAA,IAC3B,eAAA,EAAiB;AAAA,GACnB;AACF;;;ACpBO,IAAM,uBAAN,MAAoD;AAAA,EAOzD,WAAA,CAAY,OAAA,GAAiC,EAAC,EAAG;AANjD,IAAA,IAAA,CAAS,IAAA,GAAO,SAAA;AAIhB,IAAA,IAAA,CAAQ,UAAA,GAAa,KAAA;AAGnB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAI,cAAA,EAAe;AAClC,IAAA,IAAA,CAAK,YAAA,GAAe,QAAQ,YAAA,IAAgB,mBAAA;AAC5C,IAAA,IAAA,CAAK,eAAe,OAAA,CAAQ,YAAA;AAE5B,IAAA,IAAI,OAAA,CAAQ,eAAe,KAAA,EAAO;AAChC,MAAA,IAAA,CAAK,QAAQ,MAAA,EAAO;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,IAAI,SAAA,GAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,QAAQ,gBAAA,EAAiB;AAAA,EACvC;AAAA,EAEA,YAAA,GAAoC;AAClC,IAAA,OAAO;AAAA,MACL,kBAAA,EAAoB,GAAA;AAAA,MACpB,WAAA,EAAa,CAAA;AAAA,MACb,WAAA,EAAa,GAAA;AAAA,MACb,eAAA,EAAiB,KAAA;AAAA,MACjB,iBAAA,EAAmB,IAAA;AAAA,MACnB,SAAA,EAAW;AAAA,KACb;AAAA,EACF;AAAA,EAEA,MAAM,KAAA,CAAM,SAAA,EAAmB,QAAA,EAAiC;AAC9D,IAAA,MAAM,OAAA,GAAU,KAAK,WAAA,EAAY;AACjC,IAAA,IAAI,CAAC,SAAS,iBAAA,EAAmB;AAEjC,IAAA,MAAM,EAAE,aAAA,EAAe,eAAA,EAAgB,GAAI,IAAA,CAAK,aAAa,SAAS,CAAA;AAEtE,IAAA,MAAM,OAAA,CAAQ,iBAAA,CAAkB,UAAA,CAAW,aAAA,EAAe;AAAA,MACxD,UAAA,EAAY,CAAA;AAAA,MACZ,QAAA;AAAA,MACA,aAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,aAAa,KAAA,EAAoC;AACrD,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAElB,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,IAAI,KAAK,UAAA,EAAY;AAErB,MAAA,IAAI,IAAA,CAAK,IAAA,KAAS,SAAA,IAAa,IAAA,CAAK,YAAY,CAAA,EAAG;AACjD,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,SAAA,EAAW,KAAK,QAAQ,CAAA;AAC9C,QAAA,MAAM,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AAAA,MACjC,CAAA,MAAO;AACL,QAAA,MAAM,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,MAAM,OAAA,GAAU,KAAK,WAAA,EAAY;AACjC,IAAA,IAAI,SAAS,iBAAA,EAAmB;AAC9B,MAAA,OAAA,CAAQ,kBAAkB,KAAA,EAAM;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA,IAAA,CAAK,QAAQ,OAAA,EAAQ;AAAA,EACvB;AAAA;AAAA,EAGA,UAAA,GAA6B;AAC3B,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEQ,WAAA,GAA8B;AACpC,IAAA,IAAI,IAAA,CAAK,iBAAiB,MAAA,EAAW;AACnC,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAY;AAC1C,MAAA,OAAO,QAAA,CAAS,KAAK,CAAC,EAAA,KAAO,GAAG,KAAA,KAAU,IAAA,CAAK,YAAY,CAAA,IAAK,IAAA;AAAA,IAClE;AACA,IAAA,OAAO,IAAA,CAAK,QAAQ,eAAA,EAAgB;AAAA,EACtC;AAAA,EAEQ,OAAO,EAAA,EAA2B;AACxC,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAAA,EACzD;AACF","file":"index.js","sourcesContent":["/** Manages gamepad connection state and provides access to connected gamepads */\nexport class GamepadManager {\n private _onConnect?: (gamepad: Gamepad) => void;\n private _onDisconnect?: (gamepad: Gamepad) => void;\n private _listening = false;\n\n /** Start listening for gamepad connections */\n listen(): void {\n if (this._listening || typeof window === 'undefined') return;\n this._listening = true;\n\n window.addEventListener('gamepadconnected', this._handleConnect);\n window.addEventListener('gamepaddisconnected', this._handleDisconnect);\n }\n\n /** Stop listening */\n unlisten(): void {\n if (!this._listening || typeof window === 'undefined') return;\n this._listening = false;\n\n window.removeEventListener('gamepadconnected', this._handleConnect);\n window.removeEventListener('gamepaddisconnected', this._handleDisconnect);\n }\n\n /** Get all currently connected gamepads */\n getGamepads(): Gamepad[] {\n if (typeof navigator === 'undefined' || !('getGamepads' in navigator)) {\n return [];\n }\n return Array.from(navigator.getGamepads()).filter(\n (gp): gp is Gamepad => gp !== null\n );\n }\n\n /** Get the first connected gamepad, or null */\n getFirstGamepad(): Gamepad | null {\n const gamepads = this.getGamepads();\n return gamepads[0] ?? null;\n }\n\n /** Set connection callback */\n onConnect(fn: (gamepad: Gamepad) => void): void {\n this._onConnect = fn;\n }\n\n /** Set disconnection callback */\n onDisconnect(fn: (gamepad: Gamepad) => void): void {\n this._onDisconnect = fn;\n }\n\n /** Check if any gamepad with haptic support is connected */\n hasHapticGamepad(): boolean {\n return this.getGamepads().some(\n (gp) => gp.vibrationActuator != null\n );\n }\n\n dispose(): void {\n this.unlisten();\n this._onConnect = undefined;\n this._onDisconnect = undefined;\n }\n\n private _handleConnect = (e: GamepadEvent) => {\n this._onConnect?.(e.gamepad);\n };\n\n private _handleDisconnect = (e: GamepadEvent) => {\n this._onDisconnect?.(e.gamepad);\n };\n}\n","/** Dual motor parameters for gamepad haptics */\nexport interface DualMotorParams {\n /** Weak (high-frequency) motor magnitude 0-1 */\n weakMagnitude: number;\n /** Strong (low-frequency) motor magnitude 0-1 */\n strongMagnitude: number;\n}\n\nexport type MotorMappingFn = (intensity: number) => DualMotorParams;\n\n/**\n * Default motor mapping: light effects use weak motor, heavy use strong, medium uses both.\n */\nexport const defaultMotorMapping: MotorMappingFn = (intensity: number): DualMotorParams => {\n return {\n weakMagnitude: Math.min(intensity * 1.5, 1.0),\n strongMagnitude: Math.max(intensity - 0.3, 0),\n };\n};\n\n/**\n * Equal mapping: both motors at same intensity.\n */\nexport const equalMotorMapping: MotorMappingFn = (intensity: number): DualMotorParams => {\n return {\n weakMagnitude: intensity,\n strongMagnitude: intensity,\n };\n};\n\n/**\n * Heavy mapping: emphasizes the strong (low-frequency) motor.\n */\nexport const heavyMotorMapping: MotorMappingFn = (intensity: number): DualMotorParams => {\n return {\n weakMagnitude: intensity * 0.3,\n strongMagnitude: intensity,\n };\n};\n","import type { HapticAdapter, AdapterCapabilities, HapticStep } from '@hapticjs/core';\nimport { GamepadManager } from '../utils/gamepad-manager';\nimport { defaultMotorMapping } from '../utils/dual-motor';\nimport type { MotorMappingFn } from '../utils/dual-motor';\n\nexport interface GamepadAdapterOptions {\n /** Which gamepad index to use (default: first connected) */\n gamepadIndex?: number;\n /** Custom motor mapping function */\n motorMapping?: MotorMappingFn;\n /** Auto-listen for gamepad connections (default: true) */\n autoListen?: boolean;\n}\n\n/**\n * Gamepad haptics adapter using the GamepadHapticActuator API.\n * Supports dual-motor (weak/strong) vibration on modern controllers.\n */\nexport class GamepadHapticAdapter implements HapticAdapter {\n readonly name = 'gamepad';\n private manager: GamepadManager;\n private motorMapping: MotorMappingFn;\n private gamepadIndex?: number;\n private _cancelled = false;\n\n constructor(options: GamepadAdapterOptions = {}) {\n this.manager = new GamepadManager();\n this.motorMapping = options.motorMapping ?? defaultMotorMapping;\n this.gamepadIndex = options.gamepadIndex;\n\n if (options.autoListen !== false) {\n this.manager.listen();\n }\n }\n\n get supported(): boolean {\n return this.manager.hasHapticGamepad();\n }\n\n capabilities(): AdapterCapabilities {\n return {\n maxIntensityLevels: 100,\n minDuration: 1,\n maxDuration: 5000,\n supportsPattern: false,\n supportsIntensity: true,\n dualMotor: true,\n };\n }\n\n async pulse(intensity: number, duration: number): Promise<void> {\n const gamepad = this._getGamepad();\n if (!gamepad?.vibrationActuator) return;\n\n const { weakMagnitude, strongMagnitude } = this.motorMapping(intensity);\n\n await gamepad.vibrationActuator.playEffect('dual-rumble', {\n startDelay: 0,\n duration,\n weakMagnitude,\n strongMagnitude,\n });\n }\n\n async playSequence(steps: HapticStep[]): Promise<void> {\n this._cancelled = false;\n\n for (const step of steps) {\n if (this._cancelled) break;\n\n if (step.type === 'vibrate' && step.intensity > 0) {\n await this.pulse(step.intensity, step.duration);\n await this._delay(step.duration);\n } else {\n await this._delay(step.duration);\n }\n }\n }\n\n cancel(): void {\n this._cancelled = true;\n const gamepad = this._getGamepad();\n if (gamepad?.vibrationActuator) {\n gamepad.vibrationActuator.reset();\n }\n }\n\n dispose(): void {\n this.cancel();\n this.manager.dispose();\n }\n\n /** Get the gamepad manager for advanced usage */\n getManager(): GamepadManager {\n return this.manager;\n }\n\n private _getGamepad(): Gamepad | null {\n if (this.gamepadIndex !== undefined) {\n const gamepads = this.manager.getGamepads();\n return gamepads.find((gp) => gp.index === this.gamepadIndex) ?? null;\n }\n return this.manager.getFirstGamepad();\n }\n\n private _delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hapticjs/gamepad",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gamepad haptics adapter for Feelback — dual motor support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@hapticjs/core": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@hapticjs/core": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"haptics",
|
|
33
|
+
"gamepad",
|
|
34
|
+
"controller",
|
|
35
|
+
"vibration",
|
|
36
|
+
"rumble",
|
|
37
|
+
"dual-motor"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"clean": "rm -rf dist"
|
|
45
|
+
}
|
|
46
|
+
}
|