@hapticjs/gamepad 0.1.1 → 0.2.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 CHANGED
@@ -155,8 +155,121 @@ var GamepadHapticAdapter = class {
155
155
  }
156
156
  };
157
157
 
158
+ // src/spatial/spatial-haptics.ts
159
+ var SpatialHaptics = class {
160
+ constructor(adapter) {
161
+ this.adapter = adapter;
162
+ }
163
+ // ─── Directional ─────────────────────────────────────────
164
+ /** Vibrate left motor only (weak/high-frequency motor) */
165
+ async left(intensity = 0.7, duration = 50) {
166
+ duration = Math.max(25, duration);
167
+ await this._playDualRumble(intensity, 0, duration);
168
+ }
169
+ /** Vibrate right motor only (strong/low-frequency motor) */
170
+ async right(intensity = 0.7, duration = 50) {
171
+ duration = Math.max(25, duration);
172
+ await this._playDualRumble(0, intensity, duration);
173
+ }
174
+ /**
175
+ * Sweep vibration from one side to the other.
176
+ * Plays first side then second side with slight overlap.
177
+ */
178
+ async sweep(direction, duration = 200) {
179
+ duration = Math.max(50, duration);
180
+ const halfDuration = Math.max(25, Math.floor(duration / 2));
181
+ if (direction === "left-to-right") {
182
+ await this._playDualRumble(0.8, 0, halfDuration);
183
+ await this._delay(Math.max(25, halfDuration - 25));
184
+ await this._playDualRumble(0, 0.8, halfDuration);
185
+ } else {
186
+ await this._playDualRumble(0, 0.8, halfDuration);
187
+ await this._delay(Math.max(25, halfDuration - 25));
188
+ await this._playDualRumble(0.8, 0, halfDuration);
189
+ }
190
+ }
191
+ /** Pulsing haptic on specified side */
192
+ async pulse(side = "both", count = 3, interval = 100) {
193
+ interval = Math.max(50, interval);
194
+ const pulseDuration = Math.max(25, Math.floor(interval * 0.4));
195
+ for (let i = 0; i < count; i++) {
196
+ if (side === "left") {
197
+ await this._playDualRumble(0.7, 0, pulseDuration);
198
+ } else if (side === "right") {
199
+ await this._playDualRumble(0, 0.7, pulseDuration);
200
+ } else {
201
+ await this._playDualRumble(0.7, 0.7, pulseDuration);
202
+ }
203
+ if (i < count - 1) {
204
+ await this._delay(interval - pulseDuration);
205
+ }
206
+ }
207
+ }
208
+ // ─── Rumble ──────────────────────────────────────────────
209
+ /** Sustained left rumble */
210
+ async rumbleLeft(duration = 300, intensity = 0.6) {
211
+ duration = Math.max(25, duration);
212
+ await this._playDualRumble(intensity, 0, duration);
213
+ }
214
+ /** Sustained right rumble */
215
+ async rumbleRight(duration = 300, intensity = 0.6) {
216
+ duration = Math.max(25, duration);
217
+ await this._playDualRumble(0, intensity, duration);
218
+ }
219
+ // ─── Impact ──────────────────────────────────────────────
220
+ /**
221
+ * Directional impact feedback.
222
+ * Center hits both motors simultaneously.
223
+ */
224
+ async impact(direction, force = 0.9) {
225
+ const impactDuration = 50;
226
+ if (direction === "left") {
227
+ await this._playDualRumble(force, force * 0.2, impactDuration);
228
+ } else if (direction === "right") {
229
+ await this._playDualRumble(force * 0.2, force, impactDuration);
230
+ } else {
231
+ await this._playDualRumble(force, force, impactDuration);
232
+ }
233
+ }
234
+ // ─── Simulation ──────────────────────────────────────────
235
+ /**
236
+ * Engine RPM simulation.
237
+ * Low RPM = slow weak pulses, high RPM = fast strong pulses.
238
+ * RPM range: 0-8000.
239
+ */
240
+ async engine(rpm) {
241
+ const normalizedRPM = Math.min(8e3, Math.max(0, rpm));
242
+ const rpmFactor = normalizedRPM / 8e3;
243
+ const weakMag = Math.min(1, 0.2 + rpmFactor * 0.5);
244
+ const strongMag = Math.min(1, rpmFactor * 0.9);
245
+ const pulseDuration = Math.max(25, Math.floor(150 - rpmFactor * 120));
246
+ await this._playDualRumble(weakMag, strongMag, pulseDuration);
247
+ }
248
+ // ─── Internal ────────────────────────────────────────────
249
+ /**
250
+ * Play a dual-rumble effect with explicit weak/strong magnitudes.
251
+ * Uses the adapter's pulse method internally, but crafts the sequence
252
+ * to target specific motors via the gamepad vibrationActuator API.
253
+ */
254
+ async _playDualRumble(weakMagnitude, strongMagnitude, duration) {
255
+ const manager = this.adapter.getManager();
256
+ const gamepad = manager.getFirstGamepad();
257
+ if (!gamepad?.vibrationActuator) return;
258
+ await gamepad.vibrationActuator.playEffect("dual-rumble", {
259
+ startDelay: 0,
260
+ duration: Math.max(25, duration),
261
+ weakMagnitude: Math.min(1, Math.max(0, weakMagnitude)),
262
+ strongMagnitude: Math.min(1, Math.max(0, strongMagnitude))
263
+ });
264
+ }
265
+ _delay(ms) {
266
+ return new Promise((resolve) => setTimeout(resolve, ms));
267
+ }
268
+ };
269
+
158
270
  exports.GamepadHapticAdapter = GamepadHapticAdapter;
159
271
  exports.GamepadManager = GamepadManager;
272
+ exports.SpatialHaptics = SpatialHaptics;
160
273
  exports.defaultMotorMapping = defaultMotorMapping;
161
274
  exports.equalMotorMapping = equalMotorMapping;
162
275
  exports.heavyMotorMapping = heavyMotorMapping;
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/utils/gamepad-manager.ts","../src/utils/dual-motor.ts","../src/adapters/gamepad-haptic.adapter.ts","../src/spatial/spatial-haptics.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;;;ACpGO,IAAM,iBAAN,MAAqB;AAAA,EAG1B,YAAY,OAAA,EAA+B;AACzC,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AAAA;AAAA;AAAA,EAKA,MAAM,IAAA,CAAK,SAAA,GAAY,GAAA,EAAK,WAAW,EAAA,EAAmB;AACxD,IAAA,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAChC,IAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,SAAA,EAAW,CAAA,EAAG,QAAQ,CAAA;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,KAAA,CAAM,SAAA,GAAY,GAAA,EAAK,WAAW,EAAA,EAAmB;AACzD,IAAA,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAChC,IAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,CAAA,EAAG,SAAA,EAAW,QAAQ,CAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,KAAA,CACJ,SAAA,EACA,QAAA,GAAW,GAAA,EACI;AACf,IAAA,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAChC,IAAA,MAAM,YAAA,GAAe,KAAK,GAAA,CAAI,EAAA,EAAI,KAAK,KAAA,CAAM,QAAA,GAAW,CAAC,CAAC,CAAA;AAE1D,IAAA,IAAI,cAAc,eAAA,EAAiB;AACjC,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA;AAC/C,MAAA,MAAM,KAAK,MAAA,CAAO,IAAA,CAAK,IAAI,EAAA,EAAI,YAAA,GAAe,EAAE,CAAC,CAAA;AACjD,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,CAAA,EAAG,GAAA,EAAK,YAAY,CAAA;AAAA,IACjD,CAAA,MAAO;AACL,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,CAAA,EAAG,GAAA,EAAK,YAAY,CAAA;AAC/C,MAAA,MAAM,KAAK,MAAA,CAAO,IAAA,CAAK,IAAI,EAAA,EAAI,YAAA,GAAe,EAAE,CAAC,CAAA;AACjD,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA;AAAA,IACjD;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,KAAA,CACJ,IAAA,GAAkC,QAClC,KAAA,GAAQ,CAAA,EACR,WAAW,GAAA,EACI;AACf,IAAA,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAChC,IAAA,MAAM,aAAA,GAAgB,KAAK,GAAA,CAAI,EAAA,EAAI,KAAK,KAAA,CAAM,QAAA,GAAW,GAAG,CAAC,CAAA;AAE7D,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,EAAO,CAAA,EAAA,EAAK;AAC9B,MAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,QAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAA,EAAK,CAAA,EAAG,aAAa,CAAA;AAAA,MAClD,CAAA,MAAA,IAAW,SAAS,OAAA,EAAS;AAC3B,QAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,CAAA,EAAG,GAAA,EAAK,aAAa,CAAA;AAAA,MAClD,CAAA,MAAO;AACL,QAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAA,EAAK,GAAA,EAAK,aAAa,CAAA;AAAA,MACpD;AAEA,MAAA,IAAI,CAAA,GAAI,QAAQ,CAAA,EAAG;AACjB,QAAA,MAAM,IAAA,CAAK,MAAA,CAAO,QAAA,GAAW,aAAa,CAAA;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,MAAM,UAAA,CAAW,QAAA,GAAW,GAAA,EAAK,YAAY,GAAA,EAAoB;AAC/D,IAAA,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAChC,IAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,SAAA,EAAW,CAAA,EAAG,QAAQ,CAAA;AAAA,EACnD;AAAA;AAAA,EAGA,MAAM,WAAA,CAAY,QAAA,GAAW,GAAA,EAAK,YAAY,GAAA,EAAoB;AAChE,IAAA,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAChC,IAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,CAAA,EAAG,SAAA,EAAW,QAAQ,CAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,MAAA,CACJ,SAAA,EACA,KAAA,GAAQ,GAAA,EACO;AACf,IAAA,MAAM,cAAA,GAAiB,EAAA;AAEvB,IAAA,IAAI,cAAc,MAAA,EAAQ;AACxB,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,KAAA,EAAO,KAAA,GAAQ,KAAK,cAAc,CAAA;AAAA,IAC/D,CAAA,MAAA,IAAW,cAAc,OAAA,EAAS;AAChC,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,KAAA,GAAQ,GAAA,EAAK,OAAO,cAAc,CAAA;AAAA,IAC/D,CAAA,MAAO;AACL,MAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,KAAA,EAAO,KAAA,EAAO,cAAc,CAAA;AAAA,IACzD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,aAAA,GAAgB,KAAK,GAAA,CAAI,GAAA,EAAM,KAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAC,CAAA;AACrD,IAAA,MAAM,YAAY,aAAA,GAAgB,GAAA;AAGlC,IAAA,MAAM,UAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAA,GAAM,YAAY,GAAG,CAAA;AACjD,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,YAAY,GAAG,CAAA;AAG7C,IAAA,MAAM,aAAA,GAAgB,KAAK,GAAA,CAAI,EAAA,EAAI,KAAK,KAAA,CAAM,GAAA,GAAM,SAAA,GAAY,GAAG,CAAC,CAAA;AAEpE,IAAA,MAAM,IAAA,CAAK,eAAA,CAAgB,OAAA,EAAS,SAAA,EAAW,aAAa,CAAA;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,eAAA,CACZ,aAAA,EACA,eAAA,EACA,QAAA,EACe;AAGf,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,UAAA,EAAW;AACxC,IAAA,MAAM,OAAA,GAAU,QAAQ,eAAA,EAAgB;AACxC,IAAA,IAAI,CAAC,SAAS,iBAAA,EAAmB;AAEjC,IAAA,MAAM,OAAA,CAAQ,iBAAA,CAAkB,UAAA,CAAW,aAAA,EAAe;AAAA,MACxD,UAAA,EAAY,CAAA;AAAA,MACZ,QAAA,EAAU,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,QAAQ,CAAA;AAAA,MAC/B,aAAA,EAAe,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,aAAa,CAAC,CAAA;AAAA,MACrD,eAAA,EAAiB,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,eAAe,CAAC;AAAA,KAC1D,CAAA;AAAA,EACH;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.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","import type { GamepadHapticAdapter } from '../adapters/gamepad-haptic.adapter';\n\n/**\n * SpatialHaptics — directional haptic feedback for dual-motor controllers.\n *\n * Provides left/right motor control, sweeps, pulses, impacts,\n * and engine RPM simulation for immersive spatial feedback.\n */\nexport class SpatialHaptics {\n private adapter: GamepadHapticAdapter;\n\n constructor(adapter: GamepadHapticAdapter) {\n this.adapter = adapter;\n }\n\n // ─── Directional ─────────────────────────────────────────\n\n /** Vibrate left motor only (weak/high-frequency motor) */\n async left(intensity = 0.7, duration = 50): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(intensity, 0, duration);\n }\n\n /** Vibrate right motor only (strong/low-frequency motor) */\n async right(intensity = 0.7, duration = 50): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(0, intensity, duration);\n }\n\n /**\n * Sweep vibration from one side to the other.\n * Plays first side then second side with slight overlap.\n */\n async sweep(\n direction: 'left-to-right' | 'right-to-left',\n duration = 200,\n ): Promise<void> {\n duration = Math.max(50, duration);\n const halfDuration = Math.max(25, Math.floor(duration / 2));\n\n if (direction === 'left-to-right') {\n await this._playDualRumble(0.8, 0, halfDuration);\n await this._delay(Math.max(25, halfDuration - 25));\n await this._playDualRumble(0, 0.8, halfDuration);\n } else {\n await this._playDualRumble(0, 0.8, halfDuration);\n await this._delay(Math.max(25, halfDuration - 25));\n await this._playDualRumble(0.8, 0, halfDuration);\n }\n }\n\n /** Pulsing haptic on specified side */\n async pulse(\n side: 'left' | 'right' | 'both' = 'both',\n count = 3,\n interval = 100,\n ): Promise<void> {\n interval = Math.max(50, interval);\n const pulseDuration = Math.max(25, Math.floor(interval * 0.4));\n\n for (let i = 0; i < count; i++) {\n if (side === 'left') {\n await this._playDualRumble(0.7, 0, pulseDuration);\n } else if (side === 'right') {\n await this._playDualRumble(0, 0.7, pulseDuration);\n } else {\n await this._playDualRumble(0.7, 0.7, pulseDuration);\n }\n\n if (i < count - 1) {\n await this._delay(interval - pulseDuration);\n }\n }\n }\n\n // ─── Rumble ──────────────────────────────────────────────\n\n /** Sustained left rumble */\n async rumbleLeft(duration = 300, intensity = 0.6): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(intensity, 0, duration);\n }\n\n /** Sustained right rumble */\n async rumbleRight(duration = 300, intensity = 0.6): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(0, intensity, duration);\n }\n\n // ─── Impact ──────────────────────────────────────────────\n\n /**\n * Directional impact feedback.\n * Center hits both motors simultaneously.\n */\n async impact(\n direction: 'left' | 'right' | 'center',\n force = 0.9,\n ): Promise<void> {\n const impactDuration = 50;\n\n if (direction === 'left') {\n await this._playDualRumble(force, force * 0.2, impactDuration);\n } else if (direction === 'right') {\n await this._playDualRumble(force * 0.2, force, impactDuration);\n } else {\n await this._playDualRumble(force, force, impactDuration);\n }\n }\n\n // ─── Simulation ──────────────────────────────────────────\n\n /**\n * Engine RPM simulation.\n * Low RPM = slow weak pulses, high RPM = fast strong pulses.\n * RPM range: 0-8000.\n */\n async engine(rpm: number): Promise<void> {\n const normalizedRPM = Math.min(8000, Math.max(0, rpm));\n const rpmFactor = normalizedRPM / 8000;\n\n // Map RPM to motor intensities\n const weakMag = Math.min(1, 0.2 + rpmFactor * 0.5);\n const strongMag = Math.min(1, rpmFactor * 0.9);\n\n // Map RPM to pulse duration (shorter at high RPM)\n const pulseDuration = Math.max(25, Math.floor(150 - rpmFactor * 120));\n\n await this._playDualRumble(weakMag, strongMag, pulseDuration);\n }\n\n // ─── Internal ────────────────────────────────────────────\n\n /**\n * Play a dual-rumble effect with explicit weak/strong magnitudes.\n * Uses the adapter's pulse method internally, but crafts the sequence\n * to target specific motors via the gamepad vibrationActuator API.\n */\n private async _playDualRumble(\n weakMagnitude: number,\n strongMagnitude: number,\n duration: number,\n ): Promise<void> {\n // Use the adapter's playSequence with a custom motor mapping\n // by directly accessing the adapter's internal gamepad\n const manager = this.adapter.getManager();\n const gamepad = manager.getFirstGamepad();\n if (!gamepad?.vibrationActuator) return;\n\n await gamepad.vibrationActuator.playEffect('dual-rumble', {\n startDelay: 0,\n duration: Math.max(25, duration),\n weakMagnitude: Math.min(1, Math.max(0, weakMagnitude)),\n strongMagnitude: Math.min(1, Math.max(0, strongMagnitude)),\n });\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 CHANGED
@@ -76,4 +76,48 @@ declare class GamepadHapticAdapter implements HapticAdapter {
76
76
  private _delay;
77
77
  }
78
78
 
79
- export { type DualMotorParams, type GamepadAdapterOptions, GamepadHapticAdapter, GamepadManager, type MotorMappingFn, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
79
+ /**
80
+ * SpatialHaptics — directional haptic feedback for dual-motor controllers.
81
+ *
82
+ * Provides left/right motor control, sweeps, pulses, impacts,
83
+ * and engine RPM simulation for immersive spatial feedback.
84
+ */
85
+ declare class SpatialHaptics {
86
+ private adapter;
87
+ constructor(adapter: GamepadHapticAdapter);
88
+ /** Vibrate left motor only (weak/high-frequency motor) */
89
+ left(intensity?: number, duration?: number): Promise<void>;
90
+ /** Vibrate right motor only (strong/low-frequency motor) */
91
+ right(intensity?: number, duration?: number): Promise<void>;
92
+ /**
93
+ * Sweep vibration from one side to the other.
94
+ * Plays first side then second side with slight overlap.
95
+ */
96
+ sweep(direction: 'left-to-right' | 'right-to-left', duration?: number): Promise<void>;
97
+ /** Pulsing haptic on specified side */
98
+ pulse(side?: 'left' | 'right' | 'both', count?: number, interval?: number): Promise<void>;
99
+ /** Sustained left rumble */
100
+ rumbleLeft(duration?: number, intensity?: number): Promise<void>;
101
+ /** Sustained right rumble */
102
+ rumbleRight(duration?: number, intensity?: number): Promise<void>;
103
+ /**
104
+ * Directional impact feedback.
105
+ * Center hits both motors simultaneously.
106
+ */
107
+ impact(direction: 'left' | 'right' | 'center', force?: number): Promise<void>;
108
+ /**
109
+ * Engine RPM simulation.
110
+ * Low RPM = slow weak pulses, high RPM = fast strong pulses.
111
+ * RPM range: 0-8000.
112
+ */
113
+ engine(rpm: number): Promise<void>;
114
+ /**
115
+ * Play a dual-rumble effect with explicit weak/strong magnitudes.
116
+ * Uses the adapter's pulse method internally, but crafts the sequence
117
+ * to target specific motors via the gamepad vibrationActuator API.
118
+ */
119
+ private _playDualRumble;
120
+ private _delay;
121
+ }
122
+
123
+ export { type DualMotorParams, type GamepadAdapterOptions, GamepadHapticAdapter, GamepadManager, type MotorMappingFn, SpatialHaptics, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
package/dist/index.d.ts CHANGED
@@ -76,4 +76,48 @@ declare class GamepadHapticAdapter implements HapticAdapter {
76
76
  private _delay;
77
77
  }
78
78
 
79
- export { type DualMotorParams, type GamepadAdapterOptions, GamepadHapticAdapter, GamepadManager, type MotorMappingFn, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
79
+ /**
80
+ * SpatialHaptics — directional haptic feedback for dual-motor controllers.
81
+ *
82
+ * Provides left/right motor control, sweeps, pulses, impacts,
83
+ * and engine RPM simulation for immersive spatial feedback.
84
+ */
85
+ declare class SpatialHaptics {
86
+ private adapter;
87
+ constructor(adapter: GamepadHapticAdapter);
88
+ /** Vibrate left motor only (weak/high-frequency motor) */
89
+ left(intensity?: number, duration?: number): Promise<void>;
90
+ /** Vibrate right motor only (strong/low-frequency motor) */
91
+ right(intensity?: number, duration?: number): Promise<void>;
92
+ /**
93
+ * Sweep vibration from one side to the other.
94
+ * Plays first side then second side with slight overlap.
95
+ */
96
+ sweep(direction: 'left-to-right' | 'right-to-left', duration?: number): Promise<void>;
97
+ /** Pulsing haptic on specified side */
98
+ pulse(side?: 'left' | 'right' | 'both', count?: number, interval?: number): Promise<void>;
99
+ /** Sustained left rumble */
100
+ rumbleLeft(duration?: number, intensity?: number): Promise<void>;
101
+ /** Sustained right rumble */
102
+ rumbleRight(duration?: number, intensity?: number): Promise<void>;
103
+ /**
104
+ * Directional impact feedback.
105
+ * Center hits both motors simultaneously.
106
+ */
107
+ impact(direction: 'left' | 'right' | 'center', force?: number): Promise<void>;
108
+ /**
109
+ * Engine RPM simulation.
110
+ * Low RPM = slow weak pulses, high RPM = fast strong pulses.
111
+ * RPM range: 0-8000.
112
+ */
113
+ engine(rpm: number): Promise<void>;
114
+ /**
115
+ * Play a dual-rumble effect with explicit weak/strong magnitudes.
116
+ * Uses the adapter's pulse method internally, but crafts the sequence
117
+ * to target specific motors via the gamepad vibrationActuator API.
118
+ */
119
+ private _playDualRumble;
120
+ private _delay;
121
+ }
122
+
123
+ export { type DualMotorParams, type GamepadAdapterOptions, GamepadHapticAdapter, GamepadManager, type MotorMappingFn, SpatialHaptics, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
package/dist/index.js CHANGED
@@ -153,6 +153,118 @@ var GamepadHapticAdapter = class {
153
153
  }
154
154
  };
155
155
 
156
- export { GamepadHapticAdapter, GamepadManager, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
156
+ // src/spatial/spatial-haptics.ts
157
+ var SpatialHaptics = class {
158
+ constructor(adapter) {
159
+ this.adapter = adapter;
160
+ }
161
+ // ─── Directional ─────────────────────────────────────────
162
+ /** Vibrate left motor only (weak/high-frequency motor) */
163
+ async left(intensity = 0.7, duration = 50) {
164
+ duration = Math.max(25, duration);
165
+ await this._playDualRumble(intensity, 0, duration);
166
+ }
167
+ /** Vibrate right motor only (strong/low-frequency motor) */
168
+ async right(intensity = 0.7, duration = 50) {
169
+ duration = Math.max(25, duration);
170
+ await this._playDualRumble(0, intensity, duration);
171
+ }
172
+ /**
173
+ * Sweep vibration from one side to the other.
174
+ * Plays first side then second side with slight overlap.
175
+ */
176
+ async sweep(direction, duration = 200) {
177
+ duration = Math.max(50, duration);
178
+ const halfDuration = Math.max(25, Math.floor(duration / 2));
179
+ if (direction === "left-to-right") {
180
+ await this._playDualRumble(0.8, 0, halfDuration);
181
+ await this._delay(Math.max(25, halfDuration - 25));
182
+ await this._playDualRumble(0, 0.8, halfDuration);
183
+ } else {
184
+ await this._playDualRumble(0, 0.8, halfDuration);
185
+ await this._delay(Math.max(25, halfDuration - 25));
186
+ await this._playDualRumble(0.8, 0, halfDuration);
187
+ }
188
+ }
189
+ /** Pulsing haptic on specified side */
190
+ async pulse(side = "both", count = 3, interval = 100) {
191
+ interval = Math.max(50, interval);
192
+ const pulseDuration = Math.max(25, Math.floor(interval * 0.4));
193
+ for (let i = 0; i < count; i++) {
194
+ if (side === "left") {
195
+ await this._playDualRumble(0.7, 0, pulseDuration);
196
+ } else if (side === "right") {
197
+ await this._playDualRumble(0, 0.7, pulseDuration);
198
+ } else {
199
+ await this._playDualRumble(0.7, 0.7, pulseDuration);
200
+ }
201
+ if (i < count - 1) {
202
+ await this._delay(interval - pulseDuration);
203
+ }
204
+ }
205
+ }
206
+ // ─── Rumble ──────────────────────────────────────────────
207
+ /** Sustained left rumble */
208
+ async rumbleLeft(duration = 300, intensity = 0.6) {
209
+ duration = Math.max(25, duration);
210
+ await this._playDualRumble(intensity, 0, duration);
211
+ }
212
+ /** Sustained right rumble */
213
+ async rumbleRight(duration = 300, intensity = 0.6) {
214
+ duration = Math.max(25, duration);
215
+ await this._playDualRumble(0, intensity, duration);
216
+ }
217
+ // ─── Impact ──────────────────────────────────────────────
218
+ /**
219
+ * Directional impact feedback.
220
+ * Center hits both motors simultaneously.
221
+ */
222
+ async impact(direction, force = 0.9) {
223
+ const impactDuration = 50;
224
+ if (direction === "left") {
225
+ await this._playDualRumble(force, force * 0.2, impactDuration);
226
+ } else if (direction === "right") {
227
+ await this._playDualRumble(force * 0.2, force, impactDuration);
228
+ } else {
229
+ await this._playDualRumble(force, force, impactDuration);
230
+ }
231
+ }
232
+ // ─── Simulation ──────────────────────────────────────────
233
+ /**
234
+ * Engine RPM simulation.
235
+ * Low RPM = slow weak pulses, high RPM = fast strong pulses.
236
+ * RPM range: 0-8000.
237
+ */
238
+ async engine(rpm) {
239
+ const normalizedRPM = Math.min(8e3, Math.max(0, rpm));
240
+ const rpmFactor = normalizedRPM / 8e3;
241
+ const weakMag = Math.min(1, 0.2 + rpmFactor * 0.5);
242
+ const strongMag = Math.min(1, rpmFactor * 0.9);
243
+ const pulseDuration = Math.max(25, Math.floor(150 - rpmFactor * 120));
244
+ await this._playDualRumble(weakMag, strongMag, pulseDuration);
245
+ }
246
+ // ─── Internal ────────────────────────────────────────────
247
+ /**
248
+ * Play a dual-rumble effect with explicit weak/strong magnitudes.
249
+ * Uses the adapter's pulse method internally, but crafts the sequence
250
+ * to target specific motors via the gamepad vibrationActuator API.
251
+ */
252
+ async _playDualRumble(weakMagnitude, strongMagnitude, duration) {
253
+ const manager = this.adapter.getManager();
254
+ const gamepad = manager.getFirstGamepad();
255
+ if (!gamepad?.vibrationActuator) return;
256
+ await gamepad.vibrationActuator.playEffect("dual-rumble", {
257
+ startDelay: 0,
258
+ duration: Math.max(25, duration),
259
+ weakMagnitude: Math.min(1, Math.max(0, weakMagnitude)),
260
+ strongMagnitude: Math.min(1, Math.max(0, strongMagnitude))
261
+ });
262
+ }
263
+ _delay(ms) {
264
+ return new Promise((resolve) => setTimeout(resolve, ms));
265
+ }
266
+ };
267
+
268
+ export { GamepadHapticAdapter, GamepadManager, SpatialHaptics, defaultMotorMapping, equalMotorMapping, heavyMotorMapping };
157
269
  //# sourceMappingURL=index.js.map
158
270
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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.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"]}
1
+ {"version":3,"sources":["../src/utils/gamepad-manager.ts","../src/utils/dual-motor.ts","../src/adapters/gamepad-haptic.adapter.ts","../src/spatial/spatial-haptics.ts"],"names":[],"mappings":"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","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","import type { GamepadHapticAdapter } from '../adapters/gamepad-haptic.adapter';\n\n/**\n * SpatialHaptics — directional haptic feedback for dual-motor controllers.\n *\n * Provides left/right motor control, sweeps, pulses, impacts,\n * and engine RPM simulation for immersive spatial feedback.\n */\nexport class SpatialHaptics {\n private adapter: GamepadHapticAdapter;\n\n constructor(adapter: GamepadHapticAdapter) {\n this.adapter = adapter;\n }\n\n // ─── Directional ─────────────────────────────────────────\n\n /** Vibrate left motor only (weak/high-frequency motor) */\n async left(intensity = 0.7, duration = 50): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(intensity, 0, duration);\n }\n\n /** Vibrate right motor only (strong/low-frequency motor) */\n async right(intensity = 0.7, duration = 50): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(0, intensity, duration);\n }\n\n /**\n * Sweep vibration from one side to the other.\n * Plays first side then second side with slight overlap.\n */\n async sweep(\n direction: 'left-to-right' | 'right-to-left',\n duration = 200,\n ): Promise<void> {\n duration = Math.max(50, duration);\n const halfDuration = Math.max(25, Math.floor(duration / 2));\n\n if (direction === 'left-to-right') {\n await this._playDualRumble(0.8, 0, halfDuration);\n await this._delay(Math.max(25, halfDuration - 25));\n await this._playDualRumble(0, 0.8, halfDuration);\n } else {\n await this._playDualRumble(0, 0.8, halfDuration);\n await this._delay(Math.max(25, halfDuration - 25));\n await this._playDualRumble(0.8, 0, halfDuration);\n }\n }\n\n /** Pulsing haptic on specified side */\n async pulse(\n side: 'left' | 'right' | 'both' = 'both',\n count = 3,\n interval = 100,\n ): Promise<void> {\n interval = Math.max(50, interval);\n const pulseDuration = Math.max(25, Math.floor(interval * 0.4));\n\n for (let i = 0; i < count; i++) {\n if (side === 'left') {\n await this._playDualRumble(0.7, 0, pulseDuration);\n } else if (side === 'right') {\n await this._playDualRumble(0, 0.7, pulseDuration);\n } else {\n await this._playDualRumble(0.7, 0.7, pulseDuration);\n }\n\n if (i < count - 1) {\n await this._delay(interval - pulseDuration);\n }\n }\n }\n\n // ─── Rumble ──────────────────────────────────────────────\n\n /** Sustained left rumble */\n async rumbleLeft(duration = 300, intensity = 0.6): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(intensity, 0, duration);\n }\n\n /** Sustained right rumble */\n async rumbleRight(duration = 300, intensity = 0.6): Promise<void> {\n duration = Math.max(25, duration);\n await this._playDualRumble(0, intensity, duration);\n }\n\n // ─── Impact ──────────────────────────────────────────────\n\n /**\n * Directional impact feedback.\n * Center hits both motors simultaneously.\n */\n async impact(\n direction: 'left' | 'right' | 'center',\n force = 0.9,\n ): Promise<void> {\n const impactDuration = 50;\n\n if (direction === 'left') {\n await this._playDualRumble(force, force * 0.2, impactDuration);\n } else if (direction === 'right') {\n await this._playDualRumble(force * 0.2, force, impactDuration);\n } else {\n await this._playDualRumble(force, force, impactDuration);\n }\n }\n\n // ─── Simulation ──────────────────────────────────────────\n\n /**\n * Engine RPM simulation.\n * Low RPM = slow weak pulses, high RPM = fast strong pulses.\n * RPM range: 0-8000.\n */\n async engine(rpm: number): Promise<void> {\n const normalizedRPM = Math.min(8000, Math.max(0, rpm));\n const rpmFactor = normalizedRPM / 8000;\n\n // Map RPM to motor intensities\n const weakMag = Math.min(1, 0.2 + rpmFactor * 0.5);\n const strongMag = Math.min(1, rpmFactor * 0.9);\n\n // Map RPM to pulse duration (shorter at high RPM)\n const pulseDuration = Math.max(25, Math.floor(150 - rpmFactor * 120));\n\n await this._playDualRumble(weakMag, strongMag, pulseDuration);\n }\n\n // ─── Internal ────────────────────────────────────────────\n\n /**\n * Play a dual-rumble effect with explicit weak/strong magnitudes.\n * Uses the adapter's pulse method internally, but crafts the sequence\n * to target specific motors via the gamepad vibrationActuator API.\n */\n private async _playDualRumble(\n weakMagnitude: number,\n strongMagnitude: number,\n duration: number,\n ): Promise<void> {\n // Use the adapter's playSequence with a custom motor mapping\n // by directly accessing the adapter's internal gamepad\n const manager = this.adapter.getManager();\n const gamepad = manager.getFirstGamepad();\n if (!gamepad?.vibrationActuator) return;\n\n await gamepad.vibrationActuator.playEffect('dual-rumble', {\n startDelay: 0,\n duration: Math.max(25, duration),\n weakMagnitude: Math.min(1, Math.max(0, weakMagnitude)),\n strongMagnitude: Math.min(1, Math.max(0, strongMagnitude)),\n });\n }\n\n private _delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hapticjs/gamepad",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Gamepad haptics adapter for Feelback — dual motor support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -24,10 +24,10 @@
24
24
  ],
25
25
  "sideEffects": false,
26
26
  "peerDependencies": {
27
- "@hapticjs/core": "0.2.0"
27
+ "@hapticjs/core": "0.4.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@hapticjs/core": "0.2.0"
30
+ "@hapticjs/core": "0.4.0"
31
31
  },
32
32
  "keywords": [
33
33
  "haptics",