touch-gamepad
Touch/pointer-driven virtual gamepad backed by an SVG. Elements are identified
by data-part attributes (not IDs, so multiple instances work).
How it works
Load any SVG with elements whose data-part values match the standard layout:
data-part |
VirtualGamepad | Type |
|---|---|---|
left_stick / left_stick_travel |
leftStickX/Y |
stick |
right_stick / right_stick_travel |
rightStickX/Y |
stick |
A, B, X, Y |
buttonA/B/X/Y |
button |
left_bumper, right_bumper |
leftBumper, rightBumper |
button |
left_trigger, right_trigger |
leftTrigger, rightTrigger |
button |
dpad_up/down/left/right |
dpadUp/Down/Left/Right |
button |
Sticks
Touch inside a *_stick_travel region to grab the stick. The travel circle
recenters to your touch point and the knob tracks your drag. Release snaps
everything back. Deadzone and max-zone are applied so you don't need pixel
precision.
Buttons
Touch a button element to set its value to 1 and add an active CSS class.
Release sets it back to 0. Elements with data-part values not in the table
above (e.g. menu, view) still get the active class and fire the optional
onButton(part, pressed) callback.
Demo
import { gamepadSvg, TouchGamepadSource } from 'tosijs-3d'
import { elements } from 'tosijs'
const { div, pre } = elements
const pad = gamepadSvg()
const customBtns = new Set()
const source = new TouchGamepadSource(pad, {
onButton(part, pressed) {
if (pressed) customBtns.add(part)
else customBtns.delete(part)
},
})
const readout = pre({ class: 'readout' })
function update() {
const s = source.poll()
const lines = []
if (s.leftStickX || s.leftStickY)
lines.push(`L stick: ${s.leftStickX.toFixed(2)}, ${s.leftStickY.toFixed(2)}`)
if (s.rightStickX || s.rightStickY)
lines.push(`R stick: ${s.rightStickX.toFixed(2)}, ${s.rightStickY.toFixed(2)}`)
const btns = ['buttonA','buttonB','buttonX','buttonY',
'leftBumper','rightBumper','leftTrigger','rightTrigger',
'dpadUp','dpadDown','dpadLeft','dpadRight']
.filter(k => s[k] > 0)
if (btns.length) lines.push(`Buttons: ${btns.join(', ')}`)
if (customBtns.size) lines.push(`Custom: ${[...customBtns].join(', ')}`)
readout.textContent = lines.join('\n') || 'Touch or click the gamepad'
requestAnimationFrame(update)
}
update()
preview.append(div({ class: 'gamepad-demo' }, pad, readout))
.gamepad-demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.gamepad-demo svg {
width: 100%;
max-width: 400px;
cursor: pointer;
}
.gamepad-demo .readout {
font-family: ui-monospace, monospace;
font-size: 12px;
color: #222;
min-height: 2.5em;
margin: 0;
}
.gamepad-demo [data-part].active {
stroke-width: 24;
filter: brightness(1.3);
}
Usage
import { TouchGamepadSource } from 'tosijs-3d'
const svg = document.querySelector('svg.gamepad')
const source = new TouchGamepadSource(svg)
// Add to a MappedInputProvider alongside keyboard/hardware gamepad
provider.addSource(source)