@juleslois/virtual-gamepad
v0.1.1
Published
Reusable virtual touch controls and browser gamepad input source with a framework-agnostic core.
Maintainers
Readme
@juleslois/virtual-gamepad
Reusable virtual touch controls and a browser Gamepad API source for Canvas/WebGL games and React apps. The core is framework-agnostic; the React overlay is an optional wrapper.
Extracted from cmys-fight, a deterministic browser action roguelite.
Install
npm install @juleslois/virtual-gamepadPeer dependency: react >= 18 is only required for the ./react entry.
Core (framework-agnostic)
import {
calculateTouchViewportOffsets,
applyTouchViewportOffsets,
joystickVector,
pulseHaptics,
GamepadSource,
updateAxisNavigation,
createAxisNavigationState,
} from "@juleslois/virtual-gamepad";calculateTouchViewportOffsets(width, height, options?)— reserves safe gutters for a letterboxed 4:3 game viewport (aspect, scales and cluster sizes are configurable).applyTouchViewportOffsets(container, offsets)— writes the--touch-*CSS variables that the stylesheet consumes.joystickVector(clientX, clientY, bounds, radiusFactor?)— pure function that converts a pointer position into a normalized{ x, y }axis.pulseHaptics(duration?)— safenavigator.vibratewrapper.GamepadSource.poll()— returns aGamepadFrame(axes,buttons,dPad) from the first connected gamepad, with deadzone and d-pad synthesis applied. Feed the raw state into your own action mapping.updateAxisNavigation(state, x, y, now, suppressed)— menu-style navigation state machine: one immediate step, 420ms delay, 160ms repeat, neutral gate for direction changes.createAxisNavigationState()builds the initial state.
React overlay
import { VirtualTouchControls, type VirtualGamepadButton } from "@juleslois/virtual-gamepad/react";
import "@juleslois/virtual-gamepad/style.css";
const buttons: VirtualGamepadButton[] = [
{ id: "pause", slot: "start", label: "START", hapticMs: 12 },
{ id: "fire", slot: "x", label: "X" },
{ id: "interact", slot: "a", label: "A" },
{ id: "skill", slot: "b", label: "B" },
{ id: "swap", slot: "y", label: "Y" },
];
<TouchHost>
<VirtualTouchControls
buttons={buttons}
layout="right"
labelMode="gamepad"
scale={1}
onAxisChange={(x, y) => input.setAxis(x, y)}
onButtonChange={(id, down) => input.setAction(id, down)}
/>
</TouchHost>The component owns pointer capture, knob rendering, haptics and label layout. Your app maps button ids and the axis to its own semantic actions.
Styling
Import the stylesheet once (@juleslois/virtual-gamepad/style.css). The pixel
gamepad theme is driven by CSS variables:
--touch-scale— overall control scale (the component sets it from thescaleprop).--touch-bottom-offset/--touch-top-offset/--touch-side-offset— safe-area-aware edge gaps (set viaapplyTouchViewportOffsetsor manually).--face-top/--face-shadow— face button colors.
Override any .touch-* rule for a custom theme.
Development
The package lives at src/virtual-gamepad inside the cmys-fight repository.
The game itself consumes it through the package name
(@juleslois/virtual-gamepad as a file: dependency), so every build and test
run continuously validates the published artifact layout. Build and test
from the repository root:
npm run build:sdk # esbuild ESM + d.ts into src/virtual-gamepad/dist
npm run test:sdk # build + contract smoke test
npm run version:sdk [major|minor|patch] # bump package version
npm run publish:sdk:dry-run # inspect the tarball without publishing
npm run publish:sdk # build and publish to npm