Created
January 29, 2026 12:52
-
-
Save bxt/8cd59662eb7a8091b57cba2d20621630 to your computer and use it in GitHub Desktop.
Javascript Gamepad test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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