Files
archived-vdo.ninja/examples/gamecontroller.html
2025-05-09 03:02:30 -04:00

696 lines
25 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Controller Visualizer</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
background: #1a1a1a;
color: #fff;
}
#controls {
margin: 20px;
padding: 10px;
background: #333;
border-radius: 5px;
display: flex;
gap: 10px;
align-items: center;
}
select, button {
padding: 5px;
background: #444;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
}
button {
padding: 5px 10px;
background: #4CAF50;
border: none;
cursor: pointer;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 5px 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
}
button:hover { background: #45a049; }
#gamepad { width: 600px; height: 400px; }
.button { transition: fill 0.1s ease; }
.button.pressed { fill: #4CAF50; }
.stick { transition: transform 0.1s ease; }
#deviceLog {
width: 80%;
max-height: 200px;
overflow-y: auto;
background: #333;
padding: 10px;
margin: 10px;
border-radius: 5px;
font-family: monospace;
}
.controller-xbox, .controller-ps { display: none; }
.controller-xbox.active, .controller-ps.active { display: block; }
#mappingSelect { margin-left: 10px; }
.device-section {
background: #444;
padding: 10px;
margin: 5px 0;
border-radius: 5px;
}
.device-section h3 {
margin: 0 0 5px 0;
font-size: 14px;
color: #aaa;
}
.device-section.empty {
color: #888;
font-style: italic;
}
.device-option {
padding: 5px;
margin: 2px 0;
background: #555;
border-radius: 3px;
cursor: pointer;
}
.device-option:hover {
background: #666;
}
.device-option.active {
background: #4CAF50;
}
.device-option {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
margin-left: 8px;
background: #666;
transition: background-color 0.3s ease;
}
.status-indicator.connected {
background: #4CAF50;
}
.status-indicator.disconnected {
background: #f44336;
}
.device-info {
font-size: 12px;
color: #888;
margin-left: 8px;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
}
.empty small {
display: block;
margin-top: 4px;
color: #666;
}
.controller-xbox, .controller-playstation {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
position: absolute;
}
.controller-xbox.active, .controller-playstation.active {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body>
<div id="controls">
<div id="deviceList">
<div id="gamepadSection" class="device-section">
<h3>Game Controllers</h3>
<div id="gamepadDevices">
<div class="empty">
<div>No gamepads detected</div>
<small>Connect an Xbox, PlayStation, or other gamepad and press any button.</small>
</div>
</div>
</div>
<div id="hidSection" class="device-section">
<h3>Other USB Devices</h3>
<div id="hidDevices">
<div class="empty">
<div>No other devices connected</div>
<small>Click "Connect Device" to add USB game controllers.</small>
</div>
</div>
</div>
</div>
<select id="mappingSelect" style="display: none;">
<option value="">Select controller type</option>
<option value="Xbox">Xbox Controller</option>
<option value="PlayStation">PlayStation Controller</option>
</select>
<button id="connectHID" class="tooltip" data-tooltip="Connect a USB game controller">Connect Device</button>
<button id="disconnectDevice" class="tooltip" data-tooltip="Disconnect selected device" style="display: none;">Disconnect</button>
</div>
<div id="deviceLog"></div>
<svg id="gamepad" viewBox="0 0 800 500">
<g class="controller-xbox active">
<path d="M150 100 Q 300 150, 400 100 T 650 100 C 730 100, 730 250, 730 300 C 730 400, 700 450, 650 450 C 600 450, 450 480, 400 480 C 350 480, 200 450, 150 450 C 100 450, 70 400, 70 300 C 70 250, 70 100, 150 100 Z" fill="#333" stroke="#444" stroke-width="2" class="draggable"/>
<path d="M 150 100 C 70 100, 40 130, 40 200 C 40 300, 70 350, 150 400" fill="#333" stroke="#222" stroke-width="3" class="draggable"/>
<path d="M 650 100 C 730 100, 760 130, 760 200 C 760 300, 730 350, 650 400" fill="#333" stroke="#222" stroke-width="3" class="draggable"/>
<g id="xbox-dpad" transform="translate(250,330)">
<path id="dpad-disc" d="M -50 -50 L -50 50 L 50 50 L 50 -50 Z" fill="none" class="draggable"/>
<path d="M -30 -10 L -10 -10 L -10 -30 L 10 -30 L 10 -10 L 30 -10 L 30 10 L 10 10 L 10 30 L -10 30 L -10 10 L -30 10 Z" fill="#222" stroke="#111" stroke-width="2" class="draggable"/>
<rect id="dpad-up" class="button draggable" x="-15" y="-45" width="30" height="20" rx="3" fill="none" stroke="none"/>
<rect id="dpad-down" class="button draggable" x="-15" y="25" width="30" height="20" rx="3" fill="none" stroke="none"/>
<rect id="dpad-left" class="button draggable" x="-45" y="-15" width="20" height="30" rx="3" fill="none" stroke="none"/>
<rect id="dpad-right" class="button draggable" x="25" y="-15" width="20" height="30" rx="3" fill="none" stroke="none"/>
</g>
<g id="xbox-face" transform="translate(550,240)">
<circle id="button-a" class="button draggable" cx="0" cy="60" r="25" fill="#0a0" stroke="#070" stroke-width="2"/>
<circle id="button-b" class="button draggable" cx="60" cy="0" r="25" fill="#a00" stroke="#700" stroke-width="2"/>
<circle id="button-x" class="button draggable" cx="-60" cy="0" r="25" fill="#00a" stroke="#007" stroke-width="2"/>
<circle id="button-y" class="button draggable" cx="0" cy="-60" r="25" fill="#aa0" stroke="#770" stroke-width="2"/>
<text x="0" y="67" fill="#fff" text-anchor="middle" font-size="22" font-weight="bold" font-family="Arial" class="draggable">A</text>
<text x="60" y="7" fill="#fff" text-anchor="middle" font-size="22" font-weight="bold" font-family="Arial" class="draggable">B</text>
<text x="-60" y="7" fill="#fff" text-anchor="middle" font-size="22" font-weight="bold" font-family="Arial" class="draggable">X</text>
<text x="0" y="-53" fill="#fff" text-anchor="middle" font-size="22" font-weight="bold" font-family="Arial" class="draggable">Y</text>
</g>
<circle cx="148.6431121826172" cy="231.15162658691406" r="40" fill="#222" stroke="#111" stroke-width="2" class="draggable"/>
<circle cx="441.5507507324219" cy="328.3466491699219" r="40" fill="#222" stroke="#111" stroke-width="2" class="draggable"/>
<g id="xbox-sticks">
<circle id="stick-left" class="stick draggable" cx="149.55532836914062" cy="232.06381225585938" r="30" fill="#666" stroke="#444" stroke-width="3"/>
<circle id="stick-right" class="stick draggable" cx="442.46295166015625" cy="329.25885009765625" r="30" fill="#666" stroke="#444" stroke-width="3"/>
</g>
<g id="xbox-shoulders">
<rect id="button-lb" class="button draggable" x="95.26795959472656" y="90.06841278076172" width="100" height="25" rx="5" ry="15" fill="#444" stroke="#222" stroke-width="2"/>
<rect id="button-rb" class="button draggable" x="571.892822265625" y="81.85861206054688" width="100" height="25" rx="5" ry="15" fill="#444" stroke="#222" stroke-width="2"/>
</g>
<circle cx="349.8289794921875" cy="183.11287689208984" r="25" fill="#107c10" stroke="#fff" stroke-width="3" class="draggable"/>
<text x="348.00457763671875" y="193.84947967529297" fill="#fff" text-anchor="middle" font-size="32" font-weight="bold" font-family="Arial" class="draggable">X</text>
</g>
<g class="controller-playstation">
<!-- PS Base -->
<path d="M150 100 C150 50, 650 50, 650 100 L650 400 C650 450, 150 450, 150 400 Z" fill="#333" stroke="#444" stroke-width="2" class="draggable"/>
<!-- D-pad -->
<g id="ps-dpad" transform="translate(230,220)">
<!-- Keep the existing paths but update positions to match Xbox layout -->
<path id="ps-dpad-up" class="button draggable" d="M-15 -45 L15 -45 L15 -25 L-15 -25 Z" fill="#666"/>
<path id="ps-dpad-right" class="button draggable" d="M25 -15 L45 -15 L45 15 L25 15 Z" fill="#666"/>
<path id="ps-dpad-down" class="button draggable" d="M-15 25 L15 25 L15 45 L-15 45 Z" fill="#666"/>
<path id="ps-dpad-left" class="button draggable" d="M-45 -15 L-25 -15 L-25 15 L-45 15 Z" fill="#666"/>
</g>
<!-- Face buttons -->
<g id="ps-face" transform="translate(540,230)">
<!-- Update circle positions to match Xbox spacing -->
<circle id="ps-cross" class="button draggable" cx="0" cy="60" r="25" fill="#666"/>
<circle id="ps-circle" class="button draggable" cx="60" cy="0" r="25" fill="#666"/>
<circle id="ps-square" class="button draggable" cx="-60" cy="0" r="25" fill="#666"/>
<circle id="ps-triangle" class="button draggable" cx="0" cy="-60" r="25" fill="#666"/>
<!-- Update symbol positions to match new circle positions -->
<path d="M-8 60 L8 60 M0 52 L0 68" stroke="#444" stroke-width="3" class="draggable"/>
<circle cx="60" cy="0" r="15" stroke="#444" stroke-width="3" fill="none" class="draggable"/>
<rect x="-70" y="-10" width="20" height="20" stroke="#444" stroke-width="3" fill="none" class="draggable"/>
<path d="M0 -68 L-8 -52 L8 -52 Z" fill="#444" class="draggable"/>
</g>
<!-- Sticks base rings -->
<circle cx="297.3318176269531" cy="348.8255157470703" r="40" fill="#444" stroke="#555" stroke-width="2" class="draggable"/>
<circle cx="462.5313720703125" cy="356.6248779296875" r="40" fill="#444" stroke="#555" stroke-width="2" class="draggable"/>
<!-- Sticks -->
<g id="ps-sticks">
<circle id="ps-stick-left" class="stick draggable" cx="297.3318176269531" cy="348.82554626464844" r="30" fill="#888"/>
<circle id="ps-stick-right" class="stick draggable" cx="460.70697021484375" cy="357.53704833984375" r="30" fill="#888"/>
</g>
<!-- Shoulder buttons -->
<g id="ps-shoulders">
<rect id="ps-l1" class="button draggable" x="200" y="50" width="80" height="30" rx="15" fill="#666"/>
<rect id="ps-r1" class="button draggable" x="520" y="50" width="80" height="30" rx="15" fill="#666"/>
</g>
</g>
</svg>
<script>
const deviceSelect = document.getElementById('deviceSelect');
const connectButton = document.getElementById('connectHID');
const disconnectButton = document.getElementById('disconnectDevice');
const deviceLog = document.getElementById('deviceLog');
let animationFrame;
const devices = new Map();
let lastState = new Uint8Array();
let selectedDeviceId = null;
const mappingSelect = document.getElementById('mappingSelect');
let currentMapping = null;
function logDevice(message) {
const line = document.createElement('div');
line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
deviceLog.insertBefore(line, deviceLog.firstChild);
if (deviceLog.children.length > 50) deviceLog.lastChild.remove();
}
function updateDeviceOption(deviceId, deviceInfo) {
const deviceEl = document.createElement('div');
deviceEl.className = 'device-option';
const contentWrapper = document.createElement('div');
contentWrapper.style.flex = 1;
const nameEl = document.createElement('div');
nameEl.textContent = deviceId;
contentWrapper.appendChild(nameEl);
if (deviceInfo.detectedType) {
const typeEl = document.createElement('div');
typeEl.className = 'device-info';
typeEl.textContent = `Type: ${deviceInfo.detectedType}`;
contentWrapper.appendChild(typeEl);
}
deviceEl.appendChild(contentWrapper);
const indicator = document.createElement('div');
indicator.className = 'status-indicator';
if (deviceInfo.type === 'gamepad') {
const gamepad = navigator.getGamepads()[deviceInfo.device.index];
indicator.classList.toggle('connected', gamepad && gamepad.connected);
} else {
indicator.classList.toggle('connected', deviceInfo.device.opened);
}
deviceEl.appendChild(indicator);
deviceEl.onclick = () => selectDevice(deviceId);
return deviceEl;
}
function handleHIDInput(e) {
const data = new Uint8Array(e.data.buffer);
if (lastState.length) {
for (let i = 0; i < data.length; i++) {
if (data[i] !== lastState[i]) {
logDevice(`Device ${e.device.productName} Button ${i + 1} ${data[i] ? "Pressed" : "Released"}`);
const buttonMap = {
0: 'button-x',
1: 'button-y',
2: 'button-b',
3: 'button-a',
4: 'dpad-up',
5: 'dpad-right',
6: 'dpad-down',
7: 'dpad-left'
};
const elementId = buttonMap[i];
if (elementId) {
const element = document.getElementById(elementId);
if (element) {
element.classList.toggle('pressed', Boolean(data[i]));
}
}
}
}
}
lastState = data;
}
async function setupHIDDevice(device) {
try {
if (!device.opened) {
await device.open();
}
addDevice(device.productName, device, 'hid');
device.addEventListener("inputreport", handleHIDInput);
logDevice(`Connected to ${device.productName}`);
disconnectButton.style.display = 'inline-block';
} catch (error) {
logDevice(`Error setting up device: ${error.message}`);
}
}
connectButton.onclick = async () => {
try {
const devices = await navigator.hid.requestDevice({
filters: [] // Let Chrome show all HID devices
});
for (const device of devices) {
await setupHIDDevice(device);
}
} catch (error) {
logDevice(`Error connecting device: ${error.message}`);
}
};
const controllerMappings = {
'Xbox': {
type: 'gamepad',
buttons: {
0: 'button-a',
1: 'button-b',
2: 'button-x',
3: 'button-y',
4: 'button-lb',
5: 'button-rb',
12: 'dpad-up',
13: 'dpad-down',
14: 'dpad-left',
15: 'dpad-right'
},
sticks: {
leftX: 0,
leftY: 1,
rightX: 2,
rightY: 3
},
detect: (id) => /xbox|xinput/i.test(id)
},
'PlayStation': {
type: 'gamepad',
buttons: {
0: 'ps-cross',
1: 'ps-circle',
2: 'ps-square',
3: 'ps-triangle',
4: 'ps-l1',
5: 'ps-r1',
12: 'ps-dpad-up',
13: 'ps-dpad-down',
14: 'ps-dpad-left',
15: 'ps-dpad-right'
},
sticks: {
leftX: 0,
leftY: 1,
rightX: 2,
rightY: 3
},
detect: (id) => /playstation|ps4|ps5|dualshock|dualsense/i.test(id)
}
};
function setControllerType(type) {
const xboxController = document.querySelector('.controller-xbox');
const psController = document.querySelector('.controller-playstation');
if (!xboxController || !psController) return;
xboxController.classList.remove('active');
psController.classList.remove('active');
if (type) {
if (type.toLowerCase() === 'xbox') {
xboxController.classList.add('active');
} else if (type.toLowerCase() === 'playstation') {
psController.classList.add('active');
}
currentMapping = controllerMappings[type];
mappingSelect.value = type;
}
}
function detectControllerType(id) {
for (const [type, mapping] of Object.entries(controllerMappings)) {
if (mapping.detect(id)) return type;
}
return null;
}
function addDevice(id, device, type) {
const detectedType = detectControllerType(id);
devices.set(id, { device, type, detectedType });
const gamepadDevices = document.getElementById('gamepadDevices');
const hidDevices = document.getElementById('hidDevices');
gamepadDevices.innerHTML = '';
hidDevices.innerHTML = '';
let hasGamepads = false;
let hasHID = false;
// Sort and group devices
const deviceArray = Array.from(devices.entries()).sort((a, b) => {
if (a[1].type !== b[1].type) return a[1].type === 'gamepad' ? -1 : 1;
if (!!a[1].detectedType !== !!b[1].detectedType) return a[1].detectedType ? -1 : 1;
return a[0].localeCompare(b[0]);
});
deviceArray.forEach(([deviceId, deviceInfo]) => {
const deviceEl = updateDeviceOption(deviceId, deviceInfo);
if (deviceInfo.type === 'gamepad') {
hasGamepads = true;
gamepadDevices.appendChild(deviceEl);
} else {
hasHID = true;
hidDevices.appendChild(deviceEl);
}
});
// Update empty states
if (!hasGamepads) {
gamepadDevices.innerHTML = `
<div class="empty">
No gamepads detected. Connect a controller and press any button.
<br><small>Supports Xbox, PlayStation, and other standard gamepads.</small>
</div>`;
}
if (!hasHID) {
hidDevices.innerHTML = `
<div class="empty">
No other devices connected.
<br><small>Click "Connect Device" to add USB game controllers.</small>
</div>`;
}
if (!selectedDeviceId && deviceArray.length > 0) {
const firstGamepad = deviceArray.find(([_, info]) => info.type === 'gamepad');
if (firstGamepad) selectDevice(firstGamepad[0]);
}
updateConnectButton();
}
function updateButtonVisuals(elementId, isPressed, value = 1) {
const element = document.getElementById(elementId);
if (!element) return;
element.classList.toggle('pressed', isPressed);
if (isPressed) {
// Add visual pressure feedback
const intensity = Math.min(value * 255, 255);
element.style.fill = `rgb(${76 + intensity * 0.2}, ${175 + intensity * 0.1}, ${80 + intensity * 0.1})`;
} else {
element.style.fill = '#666';
}
}
function updateConnectButton() {
const hasDevices = devices.size > 0;
const anyHIDDevices = Array.from(devices.values()).some(d => d.type === 'hid');
connectButton.classList.toggle('pulse', !hasDevices);
connectButton.textContent = hasDevices ? 'Add Device' : 'Connect Device';
if (anyHIDDevices) {
disconnectButton.style.display = 'inline-block';
} else {
disconnectButton.style.display = 'none';
}
}
function selectDevice(deviceId) {
const deviceInfo = devices.get(deviceId);
if (!deviceInfo) return;
selectedDeviceId = deviceId;
// Update UI
document.querySelectorAll('.device-option').forEach(el =>
el.classList.toggle('active', el.textContent === deviceId)
);
if (deviceInfo.detectedType) {
setControllerType(deviceInfo.detectedType);
mappingSelect.style.display = 'none';
} else {
mappingSelect.style.display = 'inline-block';
}
startPolling();
}
function removeDevice(id) {
devices.delete(id);
if (selectedDeviceId === id) {
selectedDeviceId = null;
stopPolling();
}
}
window.addEventListener('gamepadconnected', (e) => {
logDevice(`Gamepad connected: ${e.gamepad.id}`);
addDevice(e.gamepad.id, e.gamepad, 'gamepad');
});
window.addEventListener('gamepaddisconnected', (e) => {
logDevice(`Gamepad disconnected: ${e.gamepad.id}`);
removeDevice(e.gamepad.id);
});
disconnectButton.onclick = async () => {
const deviceInfo = devices.get(selectedDeviceId);
if (deviceInfo && deviceInfo.type === 'hid') {
await deviceInfo.device.close();
removeDevice(selectedDeviceId);
disconnectButton.style.display = 'none';
logDevice(`Disconnected ${selectedDeviceId}`);
}
};
function startPolling() {
if (!animationFrame) {
animationFrame = requestAnimationFrame(updateVisuals);
}
}
function stopPolling() {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
resetVisuals();
}
function updateVisuals() {
const deviceInfo = devices.get(selectedDeviceId);
if (deviceInfo && deviceInfo.type === 'gamepad') {
updateGamepadVisuals(deviceInfo.device);
}
animationFrame = requestAnimationFrame(updateVisuals);
}
mappingSelect.addEventListener('change', () => {
if (mappingSelect.value) {
setControllerType(mappingSelect.value);
}
});
function updateGamepadVisuals(gamepadId) {
const gamepad = navigator.getGamepads()[gamepadId.index];
if (!gamepad || !currentMapping) return;
gamepad.buttons.forEach((button, index) => {
const elementId = currentMapping.buttons[index];
if (elementId) {
updateButtonVisuals(elementId, button.pressed, button.value);
}
});
const stickIds = currentMapping === controllerMappings['PlayStation'] ?
['ps-stick-left', 'ps-stick-right'] :
['stick-left', 'stick-right'];
updateStick(stickIds[0],
gamepad.axes[currentMapping.sticks.leftX],
gamepad.axes[currentMapping.sticks.leftY]
);
updateStick(stickIds[1],
gamepad.axes[currentMapping.sticks.rightX],
gamepad.axes[currentMapping.sticks.rightY]
);
}
function updateStick(stickId, x, y) {
const stick = document.getElementById(stickId);
const maxOffset = 20;
const transformX = x * maxOffset;
const transformY = y * maxOffset;
stick.style.transform = `translate(${transformX}px, ${transformY}px)`;
}
function resetVisuals() {
document.querySelectorAll('.button').forEach(button => {
button.classList.remove('pressed');
});
document.querySelectorAll('.stick').forEach(stick => {
stick.style.transform = 'translate(0, 0)';
});
}
// Check for any already-connected HID devices
async function initializeHID() {
const devices = await navigator.hid.getDevices();
devices.forEach(device => {
logDevice(`Found HID: ${device.productName}`);
if (device.opened) {
setupHIDDevice(device);
}
});
}
document.addEventListener('DOMContentLoaded', initializeHID);
</script>
</body>
</html>