Skip to content

Instantly share code, notes, and snippets.

@bxt
Created January 29, 2026 12:52
Show Gist options
  • Select an option

  • Save bxt/8cd59662eb7a8091b57cba2d20621630 to your computer and use it in GitHub Desktop.

Select an option

Save bxt/8cd59662eb7a8091b57cba2d20621630 to your computer and use it in GitHub Desktop.
Javascript Gamepad test
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gamepad test</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="player"></div>
<div id="coplayer"></div>
<div id="playerStatus">minigun</div>
<div id="coplayerStatus">minigun</div>
<script src="script.js"></script>
</body>
</html>
console.log('Gamepad API Example');
function vectorLength([x, y]) {
return Math.sqrt(x * x + y * y);
}
function vectorNormalize([x, y]) {
const length = vectorLength([x, y]);
if (length === 0) throw new Error('Cannot normalize a zero-length vector');
return [x / length, y / length];
}
function vectorScale([x, y], scale) {
return [x * scale, y * scale];
}
function vectorAdd([x1, y1], [x2, y2]) {
return [x1 + x2, y1 + y2];
}
function vectorSubtract([x1, y1], [x2, y2]) {
return [x1 - x2, y1 - y2];
}
const SPEED = 2; // Speed of the player movement
const PROJECTILE_SPEED = 0.3; // Launch speed of the projectiles
const PROJECTILE_EVERY = 20; // Launch interval of the projectiles
const playerBase = {
lastShotTime: 0,
equipped: 'minigun',
equipChangeHandled: false,
bigShotHandled: false,
};
const thePlayer = {
name: 'thePlayer',
element: document.getElementById('player'),
statusElement: document.getElementById('playerStatus'),
position: [120, 120],
direction: [1, 0],
velocity: [0, 0],
...playerBase,
};
const coPlayer = {
name: 'coPlayer',
element: document.getElementById('coplayer'),
statusElement: document.getElementById('coplayerStatus'),
position: [220, 220],
direction: [1, 0],
velocity: [0, 0],
...playerBase,
}
const PLAYER_RADIUS = 20;
let isRumbling = false;
let lastTimestamp = null;
const tools = ['minigun', 'shotgun', 'countermeasure', 'rocketLauncher', 'mine'];
const projectiles = [];
const PROJECTILE_RADIUS = 4;
function step(timestamp) {
const gamepad = navigator.getGamepads()[0];
if (!lastTimestamp) { lastTimestamp = timestamp; requestAnimationFrame(step); return; }
const elapsed = timestamp - lastTimestamp;
lastTimestamp = timestamp;
async function doRumble() {
if (isRumbling) return;
if (!gamepad) return;
isRumbling = true;
await gamepad.vibrationActuator.playEffect("dual-rumble", {
duration: 50,
weakMagnitude: 1.0,
strongMagnitude: 1.0,
});
isRumbling = false;
}
const [, , , , leftButtonTop, rightButtonTop, leftButtonBottom, rightButtonBottom] = gamepad ? gamepad.buttons.map(b => b.pressed || b.value > 0) : [false, false, false, false, false, false, false, false];
const [leftStickX, leftStickY, rightStickX, rightStickY] = gamepad ? gamepad.axes : [0, 0, 0, 0];
for (const [pressed, player] of [
[leftButtonTop, thePlayer],
[rightButtonTop, coPlayer],
]) {
if (pressed && !player.equipChangeHandled) {
player.equipChangeHandled = true;
const currentIndex = tools.indexOf(player.equipped);
const nextIndex = (currentIndex + 1) % tools.length;
player.equipped = tools[nextIndex];
player.statusElement.textContent = player.equipped;
} else if (!pressed) {
player.equipChangeHandled = false;
}
}
thePlayer.velocity = [leftStickX, leftStickY];
coPlayer.velocity = [rightStickX, rightStickY];
for (const player of [thePlayer, coPlayer]) {
if (player.velocity[0] !== 0 || player.velocity[1] !== 0) {
player.direction = vectorNormalize(player.velocity);
}
}
for (const [pressed, player, otherPlayer] of [
[leftButtonBottom, thePlayer, coPlayer],
[rightButtonBottom, coPlayer, thePlayer],
]) {
if (!pressed) {
player.bigShotHandled = false;
continue;
}
const launchProjectile = ({ className, ...projectile }) => {
const element = document.createElement('div');
element.className = className;
document.body.appendChild(element);
projectiles.unshift({ ...projectile, element });
};
if (player.equipped === 'minigun' && timestamp - player.lastShotTime > PROJECTILE_EVERY) {
player.lastShotTime = timestamp;
const position = vectorAdd(player.position, vectorScale(player.direction, PLAYER_RADIUS));
const velocity = vectorAdd(player.velocity, vectorScale(player.direction, PROJECTILE_SPEED));
launchProjectile({ className: `projectile ${player.name}`, position, velocity, lifetime: 5000 });
} else if (player.equipped === 'shotgun' && timestamp - player.lastShotTime > (PROJECTILE_EVERY * 4)) {
player.lastShotTime = timestamp;
const position = vectorAdd(player.position, vectorScale(player.direction, PLAYER_RADIUS));
const spread = 0.1; // Spread angle for shotgun
for (let i = -2; i <= 2; i++) {
const angle = Math.atan2(player.direction[1], player.direction[0]) + i * spread;
const velocity = vectorAdd(player.velocity, vectorScale([Math.cos(angle), Math.sin(angle)], PROJECTILE_SPEED));
launchProjectile({ className: `projectile ${player.name}`, position, velocity, lifetime: 5000 });
}
} else if (player.equipped === 'countermeasure' && timestamp - player.lastShotTime > PROJECTILE_EVERY) {
player.lastShotTime = timestamp;
const spread = 0.6; // Spread angle for countermeasure
const angle = Math.atan2(player.direction[1], player.direction[0]) + (Math.random() - 0.5) * spread;
const position = vectorAdd(player.position, vectorScale(player.direction, -PLAYER_RADIUS));
const velocity = vectorAdd(player.velocity, vectorScale([Math.cos(angle), Math.sin(angle)], PROJECTILE_SPEED * -0.5));
launchProjectile({ className: `countermeasure ${player.name}`, position, velocity, lifetime: 1000, drag: 0.99 });
} else if (player.equipped === 'rocketLauncher' && !player.bigShotHandled) {
const tick = (projectile) => {
const otherPlayerPosition = otherPlayer.position;
const difference = vectorSubtract(otherPlayerPosition, projectile.position);
const differenceAngle = Math.atan2(difference[1], difference[0]);
projectile.velocity = vectorAdd(projectile.velocity, vectorScale(vectorNormalize([Math.cos(differenceAngle), Math.sin(differenceAngle)]), PROJECTILE_SPEED * 0.01));
};
player.bigShotHandled = true;
player.lastShotTime = timestamp;
const position = vectorAdd(player.position, vectorScale(player.direction, PLAYER_RADIUS));
const velocity = vectorAdd(player.velocity, vectorScale(player.direction, PROJECTILE_SPEED * 0.5));
launchProjectile({ className: `missile ${player.name}`, position, velocity, lifetime: 10000, drag: 0.99, tick });
} else if (player.equipped === 'mine' && !player.bigShotHandled) {
player.bigShotHandled = true;
player.lastShotTime = timestamp;
const position = vectorAdd(player.position, vectorScale(player.direction, -PLAYER_RADIUS));
const velocity = vectorAdd(player.velocity, vectorScale(player.direction, PROJECTILE_SPEED * -0.3));
launchProjectile({ className: `mine ${player.name}`, position, velocity, lifetime: 30000, drag: 0.9 });
}
}
for (const projectile of projectiles) {
if (projectile.drag) {
// const speed = vectorLength(projectile.velocity); squared? elapsed??? Integral?
projectile.velocity[0] *= projectile.drag;
projectile.velocity[1] *= projectile.drag;
}
if (projectile.tick) {
projectile.tick(projectile);
}
projectile.position = vectorAdd(projectile.position, vectorScale(projectile.velocity, elapsed * SPEED));
projectile.element.style.left = `${projectile.position[0] - PROJECTILE_RADIUS}px`;
projectile.element.style.top = `${projectile.position[1] - PROJECTILE_RADIUS}px`;
projectile.lifetime -= elapsed;
const isOutOfBounds = projectile.position[0] < 0 || projectile.position[0] > window.innerWidth || projectile.position[1] < 0 || projectile.position[1] > window.innerHeight;
if (isOutOfBounds || projectile.lifetime < 0) {
projectile.element.remove();
projectiles.splice(projectiles.indexOf(projectile), 1);
}
}
for (const {position, velocity, element} of [thePlayer, coPlayer]) {
position[0] += velocity[0] * elapsed * SPEED;
position[1] += velocity[1] * elapsed * SPEED;
if (position[0] < PLAYER_RADIUS) {
doRumble();
position[0] = PLAYER_RADIUS;
}
if (position[0] > window.innerWidth - PLAYER_RADIUS) {
doRumble();
position[0] = window.innerWidth - PLAYER_RADIUS;
}
if (position[1] < PLAYER_RADIUS) {
doRumble();
position[1] = PLAYER_RADIUS;
}
if (position[1] > window.innerHeight - PLAYER_RADIUS) {
doRumble();
position[1] = window.innerHeight - PLAYER_RADIUS;
}
element.style.left = `${position[0] - PLAYER_RADIUS}px`;
element.style.top = `${position[1] - PLAYER_RADIUS}px`;
}
requestAnimationFrame(step);
}
requestAnimationFrame(step);
body {
background-color: #000000;
color: #fff;
}
#player, #coplayer {
position: absolute;
width: 40px;
height: 40px;
border-radius: 20px;
}
#player {
top: 100px;
left: 100px;
background-color: #6f2c8a;
}
#coplayer {
top: 200px;
left: 200px;
background-color: #a60c82;
}
.projectile, .countermeasure {
position: absolute;
width: 8px;
height: 8px;
&.thePlayer {
background-color: #461658;
}
&.coPlayer {
background-color: #8e086f;
}
}
.projectile {
border-radius: 4px;
}
.countermeasure {
border-radius: 2px;
opacity: 0.5;
}
.missile, .mine {
position: absolute;
width: 20px;
height: 20px;
border-radius: 10px;
&.thePlayer {
background-color: #704380;
}
&.coPlayer {
background-color: #63044d;
}
}
.mine {
opacity: 0.5;
}
#playerStatus, #coplayerStatus {
position: absolute;
bottom: 10px;
}
#playerStatus {
color: #6f2c8a;
left: 10px;
}
#coplayerStatus {
color: #a60c82;
right: 10px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment