Skip to content

Instantly share code, notes, and snippets.

@zz85
Last active June 21, 2016 01:51
Show Gist options
  • Save zz85/d80e59de948e327afd704dd723ca226a to your computer and use it in GitHub Desktop.
Save zz85/d80e59de948e327afd704dd723ca226a to your computer and use it in GitHub Desktop.
Game API
<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