diff --git a/index.html b/index.html index 6b65260..ad194a6 100644 --- a/index.html +++ b/index.html @@ -65,6 +65,7 @@ + diff --git a/js/app.js b/js/app.js index 514af60..11e2764 100644 --- a/js/app.js +++ b/js/app.js @@ -1 +1 @@ -window.gamepad = new Gamepad(); \ No newline at end of file +window.gamepad = new Gamepad(); diff --git a/js/gamepad-demo.js b/js/gamepad-demo.js new file mode 100644 index 0000000..6749358 --- /dev/null +++ b/js/gamepad-demo.js @@ -0,0 +1,221 @@ +/** + * The Gamepad demo class + * + * @class GamepadDemo + */ +class GamepadDemo { + /** + * Creates an instance of GamepadDemo. + * + * @param Gamepad gamepad + */ + constructor(gamepad) { + this.gamepad = gamepad; + this.demoGamepad = { + 'id': id, + 'timestamp': 0, + 'index': 'demo', + 'mapping': 'standard', + 'axes': [], + 'buttons': [], + }; + + for (let axisIndex = 0; axisIndex < 6; axisIndex++) { + this.demoGamepad.axes[axisIndex] = 0; + } + for (let buttonIndex = 0; buttonIndex < 20; buttonIndex++) { + this.demoGamepad.buttons[buttonIndex] = { + pressed: false, + value: 0 + }; + } + } + + /** + * Starts the demonstration mode + * + * @param {string} [mode='random'] + * @param {string} [id='xinput'] + */ + start(mode = 'random', id = 'xinput') { + // remove any active gamepad + this.gamepad.removeGamepad(true); + + // add the demo gamepad to the gamepads list + this.gamepad.gamepads['demo'] = this.demoGamepad; + // map the demo gamepad as active gamepad + this.gamepad.mapGamepad('demo'); + + // determine the callback to use following the demo mode + let callback; + switch (mode) { + case 'random': + callback = this.randomModeCallback; + this.demoUpdateDelay = 100; + break; + case 'realist': + callback = this.realistModeCallback; + this.demoUpdateDelay = false; + break; + case 'sequential': + default: + callback = this.launchSequentialDemoMode; + this.controlType = 'axis'; + this.controlIndex = 0; + this.demoUpdateDelay = 1000; + break; + } + + // execute the callback once + callback.bind(this)(); + if (this.demoUpdateDelay) { + // setup an repeat callback if needed + this.demoInterval = window.setInterval( + () => { + callback.bind(this)(); + }, + this.demoUpdateDelay + ); + } + } + + /** + * Stops the demonstration mode + */ + stop() { + // stop any running demo callback + window.clearInterval(this.demoInterval); + } + + /** + * Executes the random mode callback + */ + randomModeCallback() { + // set a random value for each axis + for (let axisIndex = 0; axisIndex < this.demoGamepad.axes.length; axisIndex++) { + this.demoGamepad.axes[axisIndex] = (Math.random() * 2) - 1; + } + // set a random value for each button + for (let buttonIndex = 0; buttonIndex < this.demoGamepad.buttons.length; buttonIndex++) { + this.pressButton(buttonIndex, Math.random() > 0.5, Math.random()); + } + } + + /** + * Executes the realistic mode callback + */ + realistModeCallback() { + // TODO: capture a real usage, approx. 10s + } + + /** + * Executes the sequential mode callback + */ + sequentialModeCallback() { + // store the current index locally + const index = this.controlIndex; + + // Axis + if ('axis' === this.controlType) { + // move axis from neutral to maximum, then minimum, then neutral again + for (let i = 0; i <= 1; i = +(i + 0.01).toFixed(2)) { + let value = 0; + if (i > 0.75) { + value = (1 - i) * 4; + } else if (i > 0.25) { + value = (i - 0.5) * 4; + } else { + value = (-i) * 4; + } + + window.setTimeout( + () => { + this.moveAxis( + index, + value + ); + }, + this.demoUpdateDelay * 0.7 * i + ) + } + + // job is done for this axis, move to next axis + this.controlIndex++; + if (this.controlIndex >= this.demoGamepad.axes.length) { + // if all axis where animated, change for buttons + this.controlType = 'button'; + this.controlIndex = 0; + } + + return; + } + + if ('button' === this.controlType) { + // update button pressure from null to maximum, then release again + for (let i = 0; i <= 1; i = +(i + 0.01).toFixed(2)) { + let value = 0; + if (i > 0.5) { + value = (1 - i) * 2; + } else { + value = i * 2; + } + + window.setTimeout( + () => { + this.pressButton( + index, + value > 0, + value + ); + }, + this.demoUpdateDelay * 0.7 * i + ) + } + + // job is done for this axis, move to next button + this.controlIndex++; + if (this.controlIndex >= this.demoGamepad.buttons.length) { + // if all axis where animated, change for axes + this.controlType = 'axis'; + this.controlIndex = 0; + } + + return; + } + } + + /** + * Simulates a button press + * + * @param {any} index + * @param {boolean} [pressed=true] + * @param {number} [value=1] + */ + pressButton(index, pressed = true, value = 1) { + // no pressure means a null value + if (!pressed) { + value = 0; + } + + // update gamepad timestamp + this.demoGamepad.timestamp++; + // update button status + this.demoGamepad.buttons[index] = { + 'pressed': pressed, + 'value': value + }; + } + + /** + * Simulates an axis movement + * + * @param {any} index + * @param {number} [value=1] + */ + moveAxis(index, value = 1) { + // update gamepad timestamp + this.demoGamepad.timestamp++; + // update axis status + this.demoGamepad.axes[index] = value; + } +} diff --git a/js/gamepad.js b/js/gamepad.js index 95e9c1a..726ab1c 100644 --- a/js/gamepad.js +++ b/js/gamepad.js @@ -8,7 +8,7 @@ class Gamepad { * Creates an instance of Gamepad. */ constructor() { - this.haveEvents = 'GamepadEvent' in window; + this.gamepadDemo = new GamepadDemo(this); // cached DOM references this.$gamepad = $('.gamepad'); @@ -56,8 +56,11 @@ class Gamepad { }; // listen for gamepad related events - window.addEventListener("gamepadconnected", this.onGamepadConnect.bind(this)); - window.addEventListener("gamepaddisconnected", this.onGamepadDisconnect.bind(this)); + this.haveEvents = 'GamepadEvent' in window; + if (this.haveEvents) { + window.addEventListener("gamepadconnected", this.onGamepadConnect.bind(this)); + window.addEventListener("gamepaddisconnected", this.onGamepadDisconnect.bind(this)); + } // listen for keyboard events window.addEventListener("keydown", this.onKeyDown.bind(this)); @@ -67,6 +70,7 @@ class Gamepad { // read URI for display parameters to initalize this.params = { + demoMode: $.urlParam('demo') || null, gamepadColor: $.urlParam('color') || $.urlParam('c') || null, gamepadIndex: $.urlParam('index') || $.urlParam('i') || null, gamepadType: $.urlParam('type') || $.urlParam('t') || null, @@ -81,6 +85,12 @@ class Gamepad { return; } + if (this.params.demoMode) { + this.gamepadDemo.start(this.params.demoMode); + + return; + } + // by default, enqueue a delayed display of the help modal this.displayGamepadHelp(); } @@ -139,7 +149,7 @@ class Gamepad { switch (e.code) { case "Delete": case "Escape": - this.removeGamepad(this.activeGamepadIndex); + this.removeGamepad(true); break; case "KeyC": this.changeGamepadColor(); @@ -170,7 +180,10 @@ class Gamepad { */ refreshGamepads() { // get fresh information from DOM about gamepads - this.gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []); + const navigatorGamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []); + for (let key in navigatorGamepads) { + this.gamepads[key] = navigatorGamepads[key]; + } } /** @@ -200,8 +213,14 @@ class Gamepad { return; } + // ensure to kill demo mode + if ('demo' === this.activeGamepadIndex) { + this.gamepadDemo.stop(); + } + // if this is the active gamepad - if (gamepadIndex === this.activeGamepadIndex) { + if (true === gamepadIndex || + this.activeGamepadIndex === gamepadIndex) { // clear associated date this.activeGamepadIndex = null; this.activeGamepad = null; diff --git a/js/urlParam.jquery.js b/js/urlParam.jquery.js index 1208f85..17b0bcb 100644 --- a/js/urlParam.jquery.js +++ b/js/urlParam.jquery.js @@ -1,8 +1,8 @@ $.urlParam = function(name) { - let results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href); + let results = new RegExp('[\?&]' + name + '(=([^&#]*))?').exec(window.location.href); if (results === null) { return null; } else { - return decodeURIComponent(results[1]) || 0; + return decodeURIComponent(results[2] || true) || true; } -}; \ No newline at end of file +};