Last active
June 21, 2016 01:51
-
-
Save zz85/d80e59de948e327afd704dd723ca226a to your computer and use it in GitHub Desktop.
Game API
This file contains 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
<html> | |
<body> | |
<style> | |
body { | |
font-family: 'monospace'; | |
font-size: 12px; | |
} | |
</style> | |
Hello Copter World! | |
<div id="debug"></div> | |
<script> | |
// Author Joshua Koo twitter.com/blurspline | github.com/zz85 | |
// 21 June 2016. Drank tea. Couldn't sleep. Decided to | |
// connect bluetooth game controller, try the Game Controller API | |
// and made a crude drone / quadcopter controller | |
// Built for SteelSeries Stratus XL (Vendor: 0111 Product: 1419) | |
// You may need to press any button on your gamepad or | |
// or switch tabs to activate the controller!? | |
// Gamepad API references | |
// https://developer.mozilla.org/en-US/docs/Web/API/Gamepad | |
// https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API | |
// https://www.smashingmagazine.com/2015/11/gamepad-api-in-web-games/ | |
// http://html5gamepad.com/ | |
'use strict'; | |
var currentPad; | |
var gp; | |
var start = performance.timing.navigationStart; | |
var canvas; | |
var ctx; | |
var craft; | |
init(); | |
function init() { | |
craft = new Copter() | |
canvas = document.createElement('canvas'); | |
document.body.appendChild(canvas); | |
canvas.width = 800; | |
canvas.height = 800; | |
canvas.style.position = 'absolute'; | |
canvas.style.zIndex = 100; | |
ctx = canvas.getContext('2d'); | |
} | |
function Copter() { | |
this.altitude = 0; // z | |
this.bearing = 0; // orientation / rotation | |
this.x = 0; // gps/loc x | |
this.y = 0; // gps/loc | |
this.vx = 0; | |
this.vy = 0; | |
} | |
Copter.prototype.print = function() { | |
return `Altitude: ${this.altitude.toFixed(3)}m | |
Bearing: ${this.bearing} | |
GPS: ${this.x}, ${this.y} | |
VS, HS... | |
`; | |
}; | |
Copter.prototype.adjustHeight = function(h) { | |
var GROUND = 0; | |
this.altitude += h; | |
if (this.altitude < GROUND) { | |
this.altitude = GROUND; | |
} | |
}; | |
Copter.prototype.adjustYaw = function(v) { | |
this.bearing += v; | |
}; | |
Copter.prototype.adjustTrottleX = function(v) { | |
this.vx += Math.cos( this.bearing ) * v; | |
this.vy += Math.sin( this.bearing ) * v; | |
}; | |
Copter.prototype.adjustTrottleY = function(v) { | |
this.vx -= Math.sin( this.bearing ) * v; | |
this.vy += Math.cos( this.bearing ) * v; | |
}; | |
Copter.prototype.update = function() { | |
// should also cap acceleration | |
this.x += this.vx; | |
this.y += this.vy; | |
this.vx *= 0.95; | |
this.vy *= 0.95; | |
}; | |
window.addEventListener('gamepadconnected', function(e) { | |
console.log('Gamepad connected at index %d: %s. %d buttons, %d axes.', | |
e.gamepad.index, e.gamepad.id, | |
e.gamepad.buttons.length, e.gamepad.axes.length); | |
currentPad = e.gamepad; | |
animate(); | |
}); | |
window.addEventListener('gamepaddisconnected', function(e) { | |
console.log('Gamepad disconnected from index %d: %s', | |
e.gamepad.index, e.gamepad.id); | |
}); | |
var then = performance.now(); | |
var start = performance.now(); | |
function animate() { | |
requestAnimationFrame(animate); | |
var now = performance.now(); | |
var delta = (now - then) / 1000; // in seconds | |
then = now; | |
var gamepads = navigator.getGamepads(); | |
gp = gamepads[currentPad.index]; | |
if (gp) { | |
debug.innerHTML = ` | |
${gp.connected ? 'Connected' : 'Disconnected'}<br/> | |
Controller: ${gp.id}<br/> | |
Axes: ${gp.axes.map( (a,b)=> `#${b}: ${a.toFixed(4)} `)} <br> | |
Buttons: ${gp.buttons.map( (b, i) => `#${i}: ${b.pressed? 'on' : 'off'} ${b.value}` )}<br/> | |
Timestamp: ${new Date(start + gp.timestamp / 1000 / 1000 )}<br/> | |
Mapping: ${gp.mapping}<br/> | |
Aircrat: ${craft.print()}<br/> | |
Flight time: ${((then-start)/1000).toFixed(1)}s | |
`; | |
var RADIUS = 100; | |
var ANALOG_L_X = 200; | |
var ANALOG_L_Y = 200; | |
var ANALOG_R_X = 600; | |
var ANALOG_R_Y = 200; | |
var ANALOG_STICK_RADUIS = 4; | |
var DEAD_ZONE_THRESHOLD = 0.25; | |
// update simulation | |
var v = applyDeadzone(gp.axes[ axes_mappings.leftAnalogY ], DEAD_ZONE_THRESHOLD); | |
craft.adjustHeight( v * delta * -5 ); // vertical height 5m/s | |
v = applyDeadzone(gp.axes[ axes_mappings.leftAnalogX ], DEAD_ZONE_THRESHOLD); | |
craft.adjustYaw( v * delta * Math.PI ); // yaw, 2s for 1 full rotation | |
v = applyDeadzone( gp.axes[ axes_mappings.rightAnalogX ], DEAD_ZONE_THRESHOLD); | |
craft.adjustTrottleX( v * delta * 10 ); | |
v = applyDeadzone( gp.axes[ axes_mappings.rightAnalogY ], DEAD_ZONE_THRESHOLD); | |
craft.adjustTrottleY( v * delta * 10 ); | |
craft.update(); | |
// | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
ctx.fillStyle = '#000'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
ctx.globalCompositeOperation = 'screen'; | |
// | |
ctx.save(); | |
ctx.translate(400, 400); | |
ctx.translate(craft.x * 2, craft.y * 2); | |
ctx.rotate(craft.bearing); | |
ctx.fillStyle = '#666'; | |
ctx.beginPath(); | |
ctx.moveTo(0, -20); | |
ctx.lineTo(-20, 15); | |
ctx.lineTo(20, 15); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.fillStyle = 'red'; | |
ctx.beginPath(); | |
ctx.arc(-20, -20, 4, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc(20, -20, 4, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.fillStyle = 'pink'; | |
ctx.beginPath(); | |
ctx.arc(-20, 20, 4, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc(20, 20, 4, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc(0, 0, 15, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.restore(); | |
ctx.strokeStyle = '#35b'; | |
ctx.beginPath() | |
ctx.arc(ANALOG_L_X, ANALOG_L_Y, RADIUS, 0, Math.PI * 2); | |
ctx.stroke(); | |
ctx.beginPath() | |
ctx.arc(ANALOG_R_X, ANALOG_R_Y, RADIUS, 0, Math.PI * 2); | |
ctx.stroke(); | |
ctx.fillStyle = 'red'; | |
ctx.beginPath() | |
ctx.arc( | |
ANALOG_L_X + gp.axes[axes_mappings.leftAnalogX] * RADIUS, | |
ANALOG_L_Y + gp.axes[axes_mappings.leftAnalogY] * RADIUS, | |
ANALOG_STICK_RADUIS, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.beginPath() | |
ctx.arc( | |
ANALOG_R_X + gp.axes[axes_mappings.rightAnalogX] * RADIUS, | |
ANALOG_R_Y + gp.axes[axes_mappings.rightAnalogY] * RADIUS, | |
ANALOG_STICK_RADUIS, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.fillStyle = 'green'; | |
ctx.beginPath() | |
ctx.arc( | |
ANALOG_L_X + applyDeadzone(gp.axes[axes_mappings.leftAnalogX], DEAD_ZONE_THRESHOLD) * RADIUS, | |
ANALOG_L_Y + applyDeadzone(gp.axes[axes_mappings.leftAnalogY], DEAD_ZONE_THRESHOLD) * RADIUS, | |
ANALOG_STICK_RADUIS, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.beginPath() | |
ctx.arc( | |
ANALOG_R_X + applyDeadzone(gp.axes[axes_mappings.rightAnalogX], DEAD_ZONE_THRESHOLD) * RADIUS, | |
ANALOG_R_Y + applyDeadzone(gp.axes[axes_mappings.rightAnalogY], DEAD_ZONE_THRESHOLD) * RADIUS, | |
ANALOG_STICK_RADUIS, 0, Math.PI * 2); | |
ctx.fill(); | |
// barcharts of mapping... | |
var cols = [axes_mappings.leftAnalogX, axes_mappings.leftAnalogY, axes_mappings.rightAnalogX, axes_mappings.rightAnalogY]; | |
ctx.fillStyle = 'purple'; | |
for (var i = 0; i < cols.length; i++) { | |
var v = gp.axes[cols[i]]; | |
ctx.beginPath(); | |
var bx = 300 + i * 40; | |
var by = 400; | |
ctx.rect(bx, by, 20, RADIUS * v); | |
ctx.fill(); | |
} | |
ctx.fillStyle = 'orange'; | |
for (var i = 0; i < cols.length; i++) { | |
var v = gp.axes[cols[i]]; | |
v = applyDeadzone(v, DEAD_ZONE_THRESHOLD); | |
ctx.beginPath(); | |
var bx = 320 + i * 40; | |
var by = 400; | |
ctx.rect(bx, by, 10, RADIUS * v); | |
ctx.fill(); | |
} | |
// TODO add calibration (dead center + extreme ends) | |
// TOOD add S curve post processing | |
} | |
} | |
function applyDeadzone (number, threshold){ | |
var percentage = (Math.abs(number) - threshold) / (1 - threshold); | |
if(percentage < 0) | |
percentage = 0; | |
return percentage * (number > 0 ? 1 : -1); | |
} | |
// SOMEHOW MAPPINGS SEEMS ALL DIFFERENT IN FIREFOX!??!!??! | |
var axes_mappings = { | |
leftAnalogX: 0, // left -1, right + 1 | |
leftAnalogY: 1, // up -1, down +1 | |
rightAnalogX: 2, // up -1, down + 1 | |
rightAnalogY: 5, // left -1, right +1, | |
directional: 9, // up -1, leff 0.7, down 0.1, right -0.4 - clockwise top -1, to bottom 0, top 1 | |
L2: 4, // release -1, full press 1 | |
R2: 3, | |
}; | |
// do calibration, center, extreme ends! | |
var button_mappings = { | |
leftAnalog: 13, | |
rightAnalog: 14, | |
start: 11, | |
L1: 6, | |
R1: 7, | |
A: 0, | |
B: 1, | |
X: 3, | |
Y: 4, | |
}; | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment