glass-gamepad (b3dGamepad)

A split on-screen ("glass") gamepad for touch contexts, as a Component (so it's both the overlay element and a [[virtual-gamepad]] GamepadSource, like gameController/keyboardGamepad). Instead of one controller body, the controls are grouped into independently-anchored clusters, each a small SVG pinned to a corner of the view:

Cluster Default anchor Controls
left bottom-left left stick, d-pad, left bumper/trigger
right bottom-right A/B/X/Y, right stick, right bumper/trigger
top top-center view, menu

Each cluster is loaded from a self-contained SVG (default /gamepad-left.svg, /gamepad-right.svg, /gamepad-top.svg) whose paths are labelled by id (copied to data-part on load). There's no outer shell — just the clusters.

Usually you don't place this yourself: set the gamepad attribute on [[tosi-b3d]] and it mounts one and wires it into the active input system. Placed directly, it's a GamepadSource whose poll() merges all clusters.

import { b3dGamepad } from 'tosijs-3d'
import { elements } from 'tosijs'
const { div, pre } = elements

const pad = b3dGamepad()
const readout = pre({ class: 'readout' })

function update() {
  const s = pad.poll()
  const lines = []
  if (s.leftStickX || s.leftStickY)
    lines.push(`L: ${s.leftStickX.toFixed(2)}, ${s.leftStickY.toFixed(2)}`)
  if (s.rightStickX || s.rightStickY)
    lines.push(`R: ${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(btns.join(', '))
  readout.textContent = lines.join('\n') || 'Touch or drag the controls'
  requestAnimationFrame(update)
}
update()

// height:100% so the vmin-scaled clusters are exercised at the card's real size.
preview.append(div({ class: 'glass-stage' }, pad, readout))
.glass-stage {
  position: relative;
  height: 100%;
  min-height: 240px;
  background: radial-gradient(circle at 50% 30%, #20303a, #0b0f14);
  border-radius: 8px;
  overflow: hidden;
}
.glass-stage .readout {
  position: absolute;
  top: 12px;
  left: 12px;
  margin: 0;
  font-family: ui-monospace, monospace;
  font-size: 13px;
  color: #cfe;
  white-space: pre;
  pointer-events: none;
}