Skip to content

Commit f402d4b

Browse files
feat: add GamepadVisualizer and compatibility APIs
Introduces `GamepadVisualizer` for rendering Xbox, PlayStation, and Nintendo Switch controller artwork with live button/trigger/stick state updates. Adds `getControllerImagePath()` and `getControllerVisualConfig()` helpers, plus `getCompatibilityIssues()` for surfacing known browser/controller issues (e.g. Firefox + Switch mapping bug). All new APIs are covered by tests.
1 parent 2fa427a commit f402d4b

6 files changed

Lines changed: 918 additions & 0 deletions

File tree

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,49 @@ Helper library for Gamepad API. Helps with detecting type of gamepad and mapping
4949
npm install @lizardbyte/gamepad-helper --ignore-scripts
5050
```
5151

52+
## Controller visuals
53+
54+
`GamepadHelper` can render the packaged Xbox, PlayStation, and Nintendo Switch controller artwork and keep its buttons,
55+
triggers, and sticks synchronized with a browser `Gamepad` object. The consumer controls the asset location, color scheme,
56+
layout, and theme styling.
57+
58+
```js
59+
const gamepadHelper = new GamepadHelper();
60+
const visualizer = gamepadHelper.createVisualizer(
61+
document.getElementById('controller-visual'),
62+
{
63+
assetBasePath: '/assets/img/gamepads/',
64+
colorScheme: 'White',
65+
},
66+
);
67+
68+
visualizer.mount(gamepad);
69+
visualizer.update(navigator.getGamepads()[gamepad.index]);
70+
71+
// Remount the current controller with light-theme artwork.
72+
visualizer.setColorScheme('Black');
73+
74+
// Remove the generated DOM when the visual is no longer needed.
75+
visualizer.destroy();
76+
```
77+
78+
The renderer emits stable `gamepad-visual-*`, `gamepad-trigger-*`, and `gamepad-stick-indicator` classes for consumer
79+
styles. Custom renderers can use `getControllerVisualConfig()` and `getControllerImagePath()` without duplicating the
80+
asset-relative metadata.
81+
82+
## Compatibility issues
83+
84+
Use `getCompatibilityIssues()` to keep browser/controller compatibility knowledge in the library while presenting the
85+
warning in the consumer's own UI.
86+
87+
```js
88+
const issues = gamepadHelper.getCompatibilityIssues(gamepad);
89+
90+
issues.forEach(issue => {
91+
console.warn(issue.message, issue.issueUrl);
92+
});
93+
```
94+
5295
## Attribution
5396

5497
- Button Icons and Controls were created by Zacksly (Licensed under CC BY 3.0 - https://zacksly.itch.io)

src/js/gamepad-helper.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
* This module provides a set of utilities for working with gamepads in web applications.
44
*/
55

6+
const {
7+
GamepadVisualizer,
8+
getControllerImagePath,
9+
getControllerVisualConfig,
10+
} = require('./gamepad-visualizer');
11+
12+
const FIREFOX_SWITCH_GAMEPAD_FIXED_VERSION = 155;
13+
const FIREFOX_SWITCH_GAMEPAD_ISSUE_URL = 'https://bugzilla.mozilla.org/show_bug.cgi?id=1704419';
14+
615
/**
716
* Controller identity metadata used by browser ID lookups.
817
* @typedef {Object} GamepadIdentityMapping
@@ -372,6 +381,40 @@ class GamepadHelper {
372381
return null;
373382
}
374383

384+
/**
385+
* Get the image path for a full controller visual.
386+
* @param {string} controllerType - The type of controller (XBOX, PLAYSTATION, SWITCH)
387+
* @param {string} [basePath='/assets/img/gamepads/'] - The base path for the images
388+
* @param {string} [colorScheme='White'] - The image color ('Black' or 'White')
389+
* @returns {string|null} Encoded controller image path, or null when unavailable
390+
*/
391+
getControllerImagePath(
392+
controllerType,
393+
basePath = '/assets/img/gamepads/',
394+
colorScheme = 'White',
395+
) {
396+
return getControllerImagePath(controllerType, basePath, colorScheme);
397+
}
398+
399+
/**
400+
* Get a copy of the visual definition for a controller type.
401+
* @param {string} controllerType - The type of controller (XBOX, PLAYSTATION, SWITCH)
402+
* @returns {Object|null} Controller visual definition, or null when unavailable
403+
*/
404+
getControllerVisualConfig(controllerType) {
405+
return getControllerVisualConfig(controllerType);
406+
}
407+
408+
/**
409+
* Create a reusable visualizer in a caller-provided DOM container.
410+
* @param {Element} container - DOM element that will contain the visual
411+
* @param {Object} [options] - Visualizer options
412+
* @returns {GamepadVisualizer} Controller visualizer
413+
*/
414+
createVisualizer(container, options = {}) {
415+
return new GamepadVisualizer(this, container, options);
416+
}
417+
375418
/**
376419
* Check if the Gamepad API is supported in the current browser
377420
* @returns {boolean} True if supported, false otherwise
@@ -449,6 +492,55 @@ class GamepadHelper {
449492
return this.getGamepadInfo(gamepadId).type;
450493
}
451494

495+
/**
496+
* Extract the major Firefox version from a user-agent string.
497+
* @param {string|null} userAgent - Browser user-agent string
498+
* @returns {number|null} Firefox major version, or null for other browsers
499+
*/
500+
getFirefoxMajorVersion(userAgent) {
501+
if (typeof userAgent !== 'string') {
502+
return null;
503+
}
504+
505+
const match = /\bFirefox\/(\d+)/.exec(userAgent);
506+
return match ? Number.parseInt(match[1], 10) : null;
507+
}
508+
509+
/**
510+
* Get known browser/controller compatibility issues for a gamepad.
511+
* @param {Gamepad|null} gamepad - Gamepad to inspect
512+
* @param {Object} [options] - Detection options
513+
* @param {string} [options.userAgent=navigator.userAgent] - Browser user-agent string
514+
* @returns {Object[]} Structured compatibility issues
515+
*/
516+
getCompatibilityIssues(gamepad, options = {}) {
517+
if (!gamepad) {
518+
return [];
519+
}
520+
521+
const userAgent = options.userAgent ?? globalThis.navigator?.userAgent ?? '';
522+
const firefoxVersion = this.getFirefoxMajorVersion(userAgent);
523+
const controllerInfo = this.getGamepadInfo(gamepad.id);
524+
if (
525+
firefoxVersion === null
526+
|| firefoxVersion >= FIREFOX_SWITCH_GAMEPAD_FIXED_VERSION
527+
|| controllerInfo.type !== this.CONTROLLER_TYPES.SWITCH
528+
) {
529+
return [];
530+
}
531+
532+
return [{
533+
code: 'firefox-switch-gamepad-mapping',
534+
severity: 'warning',
535+
browser: 'firefox',
536+
browserVersion: firefoxVersion,
537+
controllerType: controllerInfo.type,
538+
fixedVersion: FIREFOX_SWITCH_GAMEPAD_FIXED_VERSION,
539+
issueUrl: FIREFOX_SWITCH_GAMEPAD_ISSUE_URL,
540+
message: `Firefox versions before ${FIREFOX_SWITCH_GAMEPAD_FIXED_VERSION} can report incorrect buttons and axes for Nintendo Switch controllers.`,
541+
}];
542+
}
543+
452544
/**
453545
* Get button name for given controller type and button index
454546
* @param {string} controllerType - The type of controller (XBOX, PLAYSTATION, SWITCH, STANDARD)
@@ -590,5 +682,7 @@ if (globalThis.window) {
590682
globalThis.GamepadHelper = GamepadHelper;
591683
}
592684

685+
GamepadHelper.GamepadVisualizer = GamepadVisualizer;
686+
593687
// Export the GamepadHelper class
594688
module.exports = GamepadHelper;

0 commit comments

Comments
 (0)