feat(): WIP add telemetry skin

This commit is contained in:
e7d 2023-02-05 12:00:07 +01:00
parent 000036e4f2
commit 203d3bf110
No known key found for this signature in database
GPG Key ID: F320BE007C0B8881
9 changed files with 589 additions and 187 deletions

17
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "docker",
"type": "shell",
"command": "docker compose up",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@ -134,6 +134,7 @@ body.unsupported #gamepad {
border-radius: 4px; border-radius: 4px;
background: whitesmoke; background: whitesmoke;
border: none; border: none;
max-width: 200px;
} }
#help-popout { #help-popout {

View File

@ -84,6 +84,12 @@
</div> </div>
<div id="gamepad"></div> <div id="gamepad"></div>
<div id="overlay" style="display: none;"> <div id="overlay" style="display: none;">
<span id="gamepad-id">
<label for="gamepad-id">Gamepad</label>
<select name="gamepad-id">
<option value="auto">Auto</option>
</select>
</span>
<span id="skin"> <span id="skin">
<label for="skin">Skin</label> <label for="skin">Skin</label>
<select name="skin"> <select name="skin">
@ -91,6 +97,7 @@
<option value="ds4">DualShock 4</option> <option value="ds4">DualShock 4</option>
<option value="xbox-one">Xbox One</option> <option value="xbox-one">Xbox One</option>
<option value="debug">Debug</option> <option value="debug">Debug</option>
<option value="telemetry">Telemetry</option>
</select> </select>
</span> </span>
<span id="background"> <span id="background">

View File

@ -9,39 +9,38 @@ class Gamepad {
*/ */
constructor() { constructor() {
// cached DOM references // cached DOM references
this.$body = $("body"); this.$body = $('body');
this.$instructions = $("#instructions"); this.$instructions = $('#instructions');
this.$placeholder = $("#placeholder"); this.$placeholder = $('#placeholder');
this.$gamepad = $("#gamepad"); this.$gamepad = $('#gamepad');
this.$overlay = $("#overlay"); this.$overlay = $('#overlay');
this.$skinSelect = $("select[name=skin]"); this.$gamepadSelect = $('select[name=gamepad-id]');
this.$backgroundSelect = $("select[name=background]"); this.$skinSelect = $('select[name=skin]');
this.$colorOverlay = this.$overlay.find("#color"); this.$backgroundSelect = $('select[name=background]');
this.$colorSelect = this.$colorOverlay.find("select[name=color]"); this.$colorOverlay = this.$overlay.find('#color');
this.$triggersOverlay = this.$overlay.find("#triggers"); this.$colorSelect = this.$colorOverlay.find('select[name=color]');
this.$triggersSelect = this.$triggersOverlay.find( this.$triggersOverlay = this.$overlay.find('#triggers');
"select[name=triggers]" this.$triggersSelect = this.$triggersOverlay.find('select[name=triggers]');
); this.$helpPopout = $('#help-popout');
this.$helpPopout = $("#help-popout"); this.$gamepadList = $('#gamepad-list');
this.$gamepadList = $("#gamepad-list");
this.backgroundStyle = [ this.backgroundStyle = [
"transparent", 'transparent',
"checkered", 'checkered',
"dimgrey", 'dimgrey',
"black", 'black',
"white", 'white',
"lime", 'lime',
"magenta", 'magenta',
]; ];
this.textColors = [ this.textColors = [
"black", 'black',
"black", 'black',
"black", 'black',
"white", 'white',
"black", 'black',
"black", 'black',
"black", 'black',
]; ];
// ensure the GamePad API is available on this browser // ensure the GamePad API is available on this browser
@ -55,40 +54,44 @@ class Gamepad {
// See: https://html5gamepad.com/codes // See: https://html5gamepad.com/codes
debug: { debug: {
id: /debug/, id: /debug/,
name: "Debug", name: 'Debug',
}, },
ds4: { ds4: {
id: /054c|54c|09cc|046d|0810|2563/, // 054c = Sony vendor code, 046d,0810,2563 = PS-like controllers vendor codes id: /054c|54c|09cc|046d|0810|2563/, // 054c = Sony vendor code, 046d,0810,2563 = PS-like controllers vendor codes
name: "DualShock 4", name: 'DualShock 4',
colors: ["black", "white", "red", "blue"], colors: ['black', 'white', 'red', 'blue'],
triggers: true, triggers: true,
}, },
// gamecube: { // gamecube: {
// id: /0079/, // 0079 = Nintendo GameCube vendor code // id: /0079/, // 0079 = Nintendo GameCube vendor code
// name: "GameCube Controller", // name: 'GameCube Controller',
// colors: ["black", "purple"], // colors: ['black', 'purple'],
// }, // },
// "joy-con": { // 'joy-con': {
// id: /200e/, // 0079 = Joy-Con specific product code // id: /200e/, // 0079 = Joy-Con specific product code
// name: "Joy-Con (L+R) Controllers", // name: 'Joy-Con (L+R) Controllers',
// colors: ["blue-red", "grey-grey"], // colors: ['blue-red', 'grey-grey'],
// }, // },
// stadia: { // stadia: {
// id: /18d1/, // 18d1 = Google vendor code // id: /18d1/, // 18d1 = Google vendor code
// name: "Stadia Controller", // name: 'Stadia Controller',
// colors: ["black"], // colors: ['black'],
// }, // },
// "switch-pro": { // 'switch-pro': {
// id: /057e|20d6/, // 057e = Nintendo Switch vendor code, 20d6 = Switch Pro-like vendor code // id: /057e|20d6/, // 057e = Nintendo Switch vendor code, 20d6 = Switch Pro-like vendor code
// name: "Switch Pro Controller", // name: 'Switch Pro Controller',
// colors: ["black"], // colors: ['black'],
// }, // },
"xbox-one": { 'xbox-one': {
id: /045e|xinput|XInput/, // 045e = Microsoft vendor code, xinput = standard Windows controller id: /045e|xinput|XInput/, // 045e = Microsoft vendor code, xinput = standard Windows controller
name: "Xbox One", name: 'Xbox One',
colors: ["black", "white"], colors: ['black', 'white'],
triggers: true, triggers: true,
}, },
'telemetry': {
id: /telemetry/, // 045e = Microsoft vendor code, xinput = standard Windows controller
name: 'Telemetry'
},
}; };
// gamepad help default values // gamepad help default values
@ -100,6 +103,7 @@ class Gamepad {
this.overlayDelay = 5000; this.overlayDelay = 5000;
// active gamepad default values // active gamepad default values
this.isFirstscan = true;
this.scanDelay = 200; this.scanDelay = 200;
this.debug = false; this.debug = false;
this.index = null; this.index = null;
@ -111,7 +115,7 @@ class Gamepad {
this.colorIndex = null; this.colorIndex = null;
this.colorName = null; this.colorName = null;
this.triggersMeter = false; this.triggersMeter = false;
this.zoomMode = "auto"; this.zoomMode = 'auto';
this.zoomLevel = 1; this.zoomLevel = 1;
this.mapping = { this.mapping = {
buttons: [], buttons: [],
@ -122,36 +126,36 @@ class Gamepad {
// this.hash = this.readHash(); // this.hash = this.readHash();
// listen for gamepad related events // listen for gamepad related events
this.haveEvents = "GamepadEvent" in window; this.haveEvents = 'GamepadEvent' in window;
if (this.haveEvents) { if (this.haveEvents) {
window.addEventListener( window.addEventListener(
"gamepadconnected", 'gamepadconnected',
this.onGamepadConnect.bind(this) this.onGamepadConnect.bind(this)
); );
window.addEventListener( window.addEventListener(
"gamepaddisconnected", 'gamepaddisconnected',
this.onGamepadDisconnect.bind(this) this.onGamepadDisconnect.bind(this)
); );
} }
// listen for mouse move events // listen for mouse move events
window.addEventListener("mousemove", this.onMouseMove.bind(this)); window.addEventListener('mousemove', this.onMouseMove.bind(this));
// listen for keyboard events // listen for keyboard events
window.addEventListener("keydown", this.onKeyDown.bind(this)); window.addEventListener('keydown', this.onKeyDown.bind(this));
// listen for keyboard events // listen for keyboard events
window.addEventListener("resize", this.onResize.bind(this)); window.addEventListener('resize', this.onResize.bind(this));
// bind a gamepads scan // bind a gamepads scan
window.setInterval(this.scan.bind(this), this.scanDelay); window.setInterval(this.scan.bind(this), this.scanDelay);
// change the type if specified // change the type if specified
const skin = this.getUrlParam("type"); const skin = this.getUrlParam('type');
if (skin) { if (skin) {
this.changeSkin(skin); this.changeSkin(skin);
} }
// change the background if specified // change the background if specified
const background = this.getUrlParam("background"); const background = this.getUrlParam('background');
if (background) { if (background) {
let backgroundStyleIndex; let backgroundStyleIndex;
for (let i = 0; i < this.backgroundStyle.length; i++) { for (let i = 0; i < this.backgroundStyle.length; i++) {
@ -179,8 +183,8 @@ class Gamepad {
? () => navigator.webkitGetGamepads() ? () => navigator.webkitGetGamepads()
: null; : null;
if (!getGamepadsFn) { if (!getGamepadsFn) {
this.$body.addClass("unsupported"); this.$body.addClass('unsupported');
throw new Error("Unsupported gamepad API"); throw new Error('Unsupported gamepad API');
} }
this.getNavigatorGamepads = getGamepadsFn; this.getNavigatorGamepads = getGamepadsFn;
} }
@ -189,17 +193,20 @@ class Gamepad {
* Initialises the overlay selectors * Initialises the overlay selectors
*/ */
initOverlaySelectors() { initOverlaySelectors() {
this.$skinSelect.on("change", () => this.$gamepadSelect.on('change', () =>
this.changeGamepad(this.$gamepadSelect.val())
);
this.$skinSelect.on('change', () =>
this.changeSkin(this.$skinSelect.val()) this.changeSkin(this.$skinSelect.val())
); );
this.$backgroundSelect.on("change", () => this.$backgroundSelect.on('change', () =>
this.changeBackgroundStyle(this.$backgroundSelect.val()) this.changeBackgroundStyle(this.$backgroundSelect.val())
); );
this.$colorSelect.on("change", () => this.$colorSelect.on('change', () =>
this.changeGamepadColor(this.$colorSelect.val()) this.changeGamepadColor(this.$colorSelect.val())
); );
this.$triggersSelect.on("change", () => this.$triggersSelect.on('change', () =>
this.toggleTriggersMeter(this.$triggersSelect.val() === "meter") this.toggleTriggersMeter(this.$triggersSelect.val() === 'meter')
); );
} }
@ -299,6 +306,25 @@ class Gamepad {
}, this.overlayDelay); }, this.overlayDelay);
} }
/**
* Updates the list of connected gamepads in the overlay
*/
updateGamepadList() {
this.$gamepadSelect.find('.entry').remove();
const $options = [];
for (let key = 0; key < this.gamepads.length; key++) {
const gamepad = this.gamepads[key];
if (!gamepad) {
continue;
}
$options.push(
`<option class='entry' value='${gamepad.id}'>${gamepad.id}</option>'`
);
}
this.$gamepadSelect.append($options.join(''));
}
/** /**
* Update colors following the active/inactive gamepad * Update colors following the active/inactive gamepad
*/ */
@ -315,7 +341,7 @@ class Gamepad {
} }
const colorOptions = colors.map( const colorOptions = colors.map(
(color) => `<option value="${color}">${color}</option>` (color) => `<option value='${color}'>${color}</option>`
); );
this.$colorSelect.html(colorOptions); this.$colorSelect.html(colorOptions);
this.$colorOverlay.fadeIn(); this.$colorOverlay.fadeIn();
@ -345,6 +371,12 @@ class Gamepad {
* @param {GamepadEvent} e * @param {GamepadEvent} e
*/ */
onGamepadConnect(e) { onGamepadConnect(e) {
// refresh gamepads information
this.pollGamepads();
// refresh gamepad list on overlay
this.updateGamepadList();
// refresh gamepad list on help, if displayed // refresh gamepad list on help, if displayed
if (this.helpVisible) this.buildHelpGamepadList(); if (this.helpVisible) this.buildHelpGamepadList();
} }
@ -355,9 +387,15 @@ class Gamepad {
* @param {GamepadEvent} e * @param {GamepadEvent} e
*/ */
onGamepadDisconnect(e) { onGamepadDisconnect(e) {
// refresh gamepads information
this.pollGamepads();
// refresh gamepad list on overlay
this.updateGamepadList();
if (e.gamepad.index === this.index) { if (e.gamepad.index === this.index) {
// display a disconnection indicator // display a disconnection indicator
this.$gamepad.addClass("disconnected"); this.$gamepad.addClass('disconnected');
this.disconnectedIndex = e.gamepad.index; this.disconnectedIndex = e.gamepad.index;
// refresh gamepad list on help, if displayed // refresh gamepad list on help, if displayed
@ -366,7 +404,7 @@ class Gamepad {
} }
/** /**
* Handles the mouse "mousemove" event * Handles the mouse 'mousemove' event
* *
* @param {MouseEvent} e * @param {MouseEvent} e
*/ */
@ -377,61 +415,61 @@ class Gamepad {
} }
/** /**
* Handles the keyboard "keydown" event * Handles the keyboard 'keydown' event
* *
* @param {KeyboardEvent} e * @param {KeyboardEvent} e
*/ */
onKeyDown(e) { onKeyDown(e) {
switch (e.code) { switch (e.code) {
case "Delete": case 'Delete':
case "Escape": case 'Escape':
this.clear(); this.clear();
this.displayPlaceholder(); this.displayPlaceholder();
break; break;
case "KeyB": case 'KeyB':
this.changeBackgroundStyle(); this.changeBackgroundStyle();
break; break;
case "KeyC": case 'KeyC':
this.changeGamepadColor(); this.changeGamepadColor();
break; break;
case "KeyD": case 'KeyD':
this.toggleDebug(); this.toggleDebug();
break; break;
case "KeyG": case 'KeyG':
this.toggleGamepadType(); this.toggleGamepadType();
break; break;
case "KeyH": case 'KeyH':
this.toggleHelp(); this.toggleHelp();
break; break;
case "KeyT": case 'KeyT':
this.toggleTriggersMeter(); this.toggleTriggersMeter();
break; break;
case "NumpadAdd": case 'NumpadAdd':
case "Equal": case 'Equal':
this.changeZoom("+"); this.changeZoom('+');
break; break;
case "NumpadSubtract": case 'NumpadSubtract':
case "Minus": case 'Minus':
this.changeZoom("-"); this.changeZoom('-');
break; break;
case "Numpad5": case 'Numpad5':
case "Digit5": case 'Digit5':
this.changeZoom("auto"); this.changeZoom('auto');
break; break;
case "Numpad0": case 'Numpad0':
case "Digit0": case 'Digit0':
this.changeZoom(0); this.changeZoom(0);
break; break;
} }
} }
/** /**
* Handles the keyboard "keydown" event * Handles the keyboard 'keydown' event
* *
* @param {WindowEvent} e * @param {WindowEvent} e
*/ */
onResize(e) { onResize(e) {
if (this.zoomMode === "auto") this.changeZoom("auto"); if (this.zoomMode === 'auto') this.changeZoom('auto');
} }
/** /**
@ -447,9 +485,6 @@ class Gamepad {
* Builds the help gamepad list * Builds the help gamepad list
*/ */
buildHelpGamepadList() { buildHelpGamepadList() {
// refresh gamepads information
this.pollGamepads();
const $tbody = []; const $tbody = [];
for (let key = 0; key < this.gamepads.length; key++) { for (let key = 0; key < this.gamepads.length; key++) {
const gamepad = this.gamepads[key]; const gamepad = this.gamepads[key];
@ -458,18 +493,20 @@ class Gamepad {
} }
$tbody.push( $tbody.push(
`<tr><td>${gamepad.index}</td><td>${gamepad.id}</td></tr>"` `<tr><td>${gamepad.index}</td><td>${gamepad.id}</td></tr>'`
); );
} }
if ($tbody.length === 0) { if ($tbody.length === 0) {
$tbody.push('<tr><td colspan="2">No gamepad detected.</td></tr>'); $tbody.push('<tr><td colspan="2">No gamepad detected.</td></tr>');
} }
this.$gamepadList.html($tbody.join("")); this.$gamepadList.html($tbody.join(''));
} }
/** /**
* Return the connected gamepad * Return the connected gamepad
*
* @returns {object}
*/ */
getActive() { getActive() {
return this.gamepads[this.index]; return this.gamepads[this.index];
@ -479,14 +516,15 @@ class Gamepad {
* Return the gamepad type for the connected gamepad * Return the gamepad type for the connected gamepad
* *
* @param {object} gamepad * @param {object} gamepad
* @returns {string}
*/ */
getType(gamepad) { getType(gamepad) {
const type = this.getUrlParam("type"); const type = this.getUrlParam('type');
// if the debug option is active, use the associated template // if the debug option is active, use the associated template
if (type === "debug") this.debug = true; if (type === 'debug') this.debug = true;
if (this.debug) { if (this.debug) {
return "debug"; return 'debug';
} }
// if the gamepad type is set through params, apply it // if the gamepad type is set through params, apply it
@ -501,7 +539,7 @@ class Gamepad {
} }
} }
return "xbox-one"; return 'xbox-one';
} }
/** /**
@ -514,6 +552,12 @@ class Gamepad {
// refresh gamepad information // refresh gamepad information
this.pollGamepads(); this.pollGamepads();
if (this.isFirstscan) {
// update the overlay list
this.updateGamepadList();
this.isFirstscan = false;
}
for (let index = 0; index < this.gamepads.length; index++) { for (let index = 0; index < this.gamepads.length; index++) {
if ( if (
null !== this.disconnectedIndex && null !== this.disconnectedIndex &&
@ -524,6 +568,15 @@ class Gamepad {
const gamepad = this.gamepads[index]; const gamepad = this.gamepads[index];
if (!gamepad) continue; if (!gamepad) continue;
// check the parameters for a selected gamepad
const gamepadId = this.getUrlParam('gamepad');
if (gamepadId) {
if (gamepad.id === gamepadId) {
this.map(gamepad.index);
return;
}
}
// read the gamepad buttons // read the gamepad buttons
let button; let button;
for ( for (
@ -563,17 +616,17 @@ class Gamepad {
*/ */
map(index) { map(index) {
// ensure a gamepad need to be mapped // ensure a gamepad need to be mapped
if ("undefined" === typeof index) return; if ('undefined' === typeof index) return;
// hide the help messages // hide the help messages
this.hideInstructions(true); this.hideInstructions(true);
this.$helpPopout.removeClass("active"); this.$helpPopout.removeClass('active');
this.hidePlaceholder(true); this.hidePlaceholder(true);
// update local references // update local references
this.index = index; this.index = index;
this.disconnectedIndex = null; this.disconnectedIndex = null;
this.$gamepad.removeClass("disconnected"); this.$gamepad.removeClass('disconnected');
const gamepad = this.getActive(); const gamepad = this.getActive();
// ensure that a gamepad was actually found for this index // ensure that a gamepad was actually found for this index
@ -594,7 +647,8 @@ class Gamepad {
// initial setup of the gamepad // initial setup of the gamepad
this.identifier = this.identifiers[this.type]; this.identifier = this.identifiers[this.type];
// update gamepad color and triggers selectors on overlay // update the overlay selectors
this.$gamepadSelect.val(gamepad.id);
this.updateColors(); this.updateColors();
this.updateTriggers(); this.updateTriggers();
@ -607,15 +661,27 @@ class Gamepad {
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "map", eventAction: 'map',
eventLabel: "Map", eventLabel: 'Map',
eventValue: this.identifier, eventValue: this.identifier,
}); });
} }
} }
/**
* Computes a SHA-1 for a given string
*
* @param {string} value
* @returns {string}
*/
async toHash(value) {
return crypto.subtle
.digest('SHA-1', new TextEncoder().encode(value))
.then(ab => encodeURIComponent(String.fromCharCode.apply(null, new Uint8Array(ab))));
}
/** /**
* Disconnect the active gamepad * Disconnect the active gamepad
* *
@ -637,16 +703,17 @@ class Gamepad {
this.colorName = null; this.colorName = null;
this.zoomLevel = 1; this.zoomLevel = 1;
this.$gamepad.empty(); this.$gamepad.empty();
this.$gamepadSelect.val('auto')
this.updateColors(); this.updateColors();
this.updateTriggers(); this.updateTriggers();
this.clearUrlParams(); this.clearUrlParams();
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "disconnect", eventAction: 'disconnect',
eventLabel: "Disconnect", eventLabel: 'Disconnect',
eventValue: this.identifier, eventValue: this.identifier,
}); });
} }
@ -667,22 +734,22 @@ class Gamepad {
// read for parameters to apply: // read for parameters to apply:
// - color // - color
this.changeGamepadColor(this.getUrlParam("color")); this.changeGamepadColor(this.getUrlParam('color'));
// - triggers mode // - triggers mode
this.toggleTriggersMeter(this.getUrlParam("triggers") === "meter"); this.toggleTriggersMeter(this.getUrlParam('triggers') === 'meter');
// - zoom$ // - zoom$
window.setTimeout(() => window.setTimeout(() =>
this.changeZoom( this.changeZoom(
this.type === "debug" this.type === 'debug'
? "auto" ? 'auto'
: this.getUrlParam("zoom") || "auto" : this.getUrlParam('zoom') || 'auto'
) )
); );
// save the buttons mapping of this template // save the buttons mapping of this template
this.mapping.buttons = []; this.mapping.buttons = [];
for (let index = 0; index < gamepad.buttons.length; index++) { for (let index = 0; index < gamepad.buttons.length; index++) {
this.mapping.buttons[index] = $(`[data-button="${index}"]`); this.mapping.buttons[index] = $(`[data-button='${index}']`);
} }
// save the axes mapping of this template // save the axes mapping of this template
@ -738,20 +805,20 @@ class Gamepad {
for (let index = 0; index < gamepad.buttons.length; index++) { for (let index = 0; index < gamepad.buttons.length; index++) {
// find the DOM element // find the DOM element
const $button = this.mapping.buttons[index]; const $button = this.mapping.buttons[index];
if (!$button) { if (!$button || $button.length === 0) {
// nothing to do for this button if no DOM element exists // nothing to do for this button if no DOM element exists
break; continue;
} }
// read the button data // read the button data
const button = gamepad.buttons[index]; const button = gamepad.buttons[index];
// update the display values // update the display values
$button.attr("data-pressed", button.pressed); $button.attr('data-pressed', button.pressed);
$button.attr("data-value", button.value); $button.attr('data-value', button.value);
// hook the template defined button update method // hook the template defined button update method
if ("function" === typeof this.updateButton) { if ('function' === typeof this.updateButton) {
this.updateButton($button); this.updateButton($button);
} }
} }
@ -767,35 +834,44 @@ class Gamepad {
for (let index = 0; index < gamepad.axes.length; index++) { for (let index = 0; index < gamepad.axes.length; index++) {
// find the DOM element // find the DOM element
const $axis = this.mapping.axes[index]; const $axis = this.mapping.axes[index];
if (!$axis) { if (!$axis || $axis.length === 0) {
// nothing to do for this button if no DOM element exists // nothing to do for this axis if no DOM element exists
break; continue;
} }
// read the axis data // read the axis data
const axis = gamepad.axes[index]; const axis = gamepad.axes[index];
// update the display values // update the display values
if ($axis.is("[data-axis=" + index + "]")) { if ($axis.is('[data-axis=' + index + ']')) {
$axis.attr("data-value", axis); $axis.attr('data-value', axis);
} }
if ($axis.is("[data-axis-x=" + index + "]")) { if ($axis.is('[data-axis-x=' + index + ']')) {
$axis.attr("data-value-x", axis); $axis.attr('data-value-x', axis);
} }
if ($axis.is("[data-axis-y=" + index + "]")) { if ($axis.is('[data-axis-y=' + index + ']')) {
$axis.attr("data-value-y", axis); $axis.attr('data-value-y', axis);
} }
if ($axis.is("[data-axis-z=" + index + "]")) { if ($axis.is('[data-axis-z=' + index + ']')) {
$axis.attr("data-value-z", axis); $axis.attr('data-value-z', axis);
} }
// hook the template defined axis update method // hook the template defined axis update method
if ("function" === typeof this.updateAxis) { if ('function' === typeof this.updateAxis) {
this.updateAxis($axis); this.updateAxis($axis);
} }
} }
} }
changeGamepad(gamepadId) {
// get the index corresponding to the identifier of the gamepad
const index = this.gamepads.findIndex(g => g && g.id === gamepadId);
// set the selected gamepad
this.updateUrlParams({ gamepad: gamepadId !== 'auto' ? gamepadId : undefined });
index === -1 ? this.clear() : this.map(index);
}
/** /**
* Changes the skin * Changes the skin
* *
@ -807,7 +883,7 @@ class Gamepad {
// set the selected skin // set the selected skin
this.debug = skin === 'debug'; this.debug = skin === 'debug';
this.updateUrlParams({ type: skin !== "auto" ? skin : undefined }); this.updateUrlParams({ type: skin !== 'auto' ? skin : undefined });
this.map(this.index); this.map(this.index);
} }
@ -817,12 +893,12 @@ class Gamepad {
* @param {any} style * @param {any} style
*/ */
changeBackgroundStyle(style) { changeBackgroundStyle(style) {
if ("undefined" === typeof style) { if ('undefined' === typeof style) {
this.backgroundStyleIndex++; this.backgroundStyleIndex++;
if (this.backgroundStyleIndex > this.backgroundStyle.length - 1) { if (this.backgroundStyleIndex > this.backgroundStyle.length - 1) {
this.backgroundStyleIndex = 0; this.backgroundStyleIndex = 0;
} }
} else if ("string" === typeof style) { } else if ('string' === typeof style) {
this.backgroundStyleIndex = this.backgroundStyle.findIndex( this.backgroundStyleIndex = this.backgroundStyle.findIndex(
(s) => s === style (s) => s === style
); );
@ -834,8 +910,8 @@ class Gamepad {
this.$body.css({ this.$body.css({
background: background:
this.backgroundStyleName === "checkered" this.backgroundStyleName === 'checkered'
? "url(css/transparent-bg.png)" ? 'url(css/transparent-bg.png)'
: this.backgroundStyleName, : this.backgroundStyleName,
color: this.textColors[this.backgroundStyleIndex], color: this.textColors[this.backgroundStyleIndex],
}); });
@ -846,10 +922,10 @@ class Gamepad {
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "change-background-color", eventAction: 'change-background-color',
eventLabel: "Change Background Color", eventLabel: 'Change Background Color',
eventValue: this.backgroundStyleName, eventValue: this.backgroundStyleName,
}); });
} }
@ -864,13 +940,13 @@ class Gamepad {
// ensure that a gamepad is currently active // ensure that a gamepad is currently active
if (this.index === null) return; if (this.index === null) return;
if ("undefined" === typeof color) { if ('undefined' === typeof color) {
// no color was specified, load the next one in list // no color was specified, load the next one in list
this.colorIndex++; this.colorIndex++;
if (this.colorIndex > this.identifier.colors.length - 1) { if (this.colorIndex > this.identifier.colors.length - 1) {
this.colorIndex = 0; this.colorIndex = 0;
} }
} else if ("string" === typeof style) { } else if ('string' === typeof style) {
this.colorIndex = this.identifier.colors.findIndex( this.colorIndex = this.identifier.colors.findIndex(
(c) => c === color (c) => c === color
); );
@ -894,7 +970,7 @@ class Gamepad {
: null; : null;
// update the DOM with the color value // update the DOM with the color value
this.$gamepad.attr("data-color", this.colorName); this.$gamepad.attr('data-color', this.colorName);
// update current settings // update current settings
this.updateUrlParams({ color: this.colorName }); this.updateUrlParams({ color: this.colorName });
@ -902,10 +978,10 @@ class Gamepad {
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "change-gamepad-color", eventAction: 'change-gamepad-color',
eventLabel: "Change Gamepad Color", eventLabel: 'Change Gamepad Color',
eventValue: this.colorName, eventValue: this.colorName,
}); });
} }
@ -921,12 +997,12 @@ class Gamepad {
if (this.index === null) return; if (this.index === null) return;
// ensure we have some data to process // ensure we have some data to process
if (typeof level === "undefined") return; if (typeof level === 'undefined') return;
this.zoomMode = level === "auto" ? "auto" : "manual"; this.zoomMode = level === 'auto' ? 'auto' : 'manual';
if (this.zoomMode === "auto") { if (this.zoomMode === 'auto') {
// "auto" means a "contained in window" zoom, with a max zoom of 1 // 'auto' means a 'contained in window' zoom, with a max zoom of 1
this.zoomLevel = Math.min( this.zoomLevel = Math.min(
window.innerWidth / this.$gamepad.width(), window.innerWidth / this.$gamepad.width(),
window.innerHeight / this.$gamepad.height(), window.innerHeight / this.$gamepad.height(),
@ -935,11 +1011,11 @@ class Gamepad {
} else if (level === 0) { } else if (level === 0) {
// 0 means a zoom reset // 0 means a zoom reset
this.zoomLevel = 1; this.zoomLevel = 1;
} else if (level === "+" && this.zoomLevel < 2) { } else if (level === '+' && this.zoomLevel < 2) {
// "+" means a zoom in if we still can // '+' means a zoom in if we still can
this.zoomLevel += 0.1; this.zoomLevel += 0.1;
} else if (level === "-" && this.zoomLevel > 0.1) { } else if (level === '-' && this.zoomLevel > 0.1) {
// "-" means a zoom out if we still can // '-' means a zoom out if we still can
this.zoomLevel -= 0.1; this.zoomLevel -= 0.1;
} else if (!isNaN((level = parseFloat(level)))) { } else if (!isNaN((level = parseFloat(level)))) {
// an integer value means a value-based zoom // an integer value means a value-based zoom
@ -951,21 +1027,21 @@ class Gamepad {
// update the DOM with the zoom value // update the DOM with the zoom value
this.$gamepad.css( this.$gamepad.css(
"transform", 'transform',
`translate(-50%, -50%) scale(${this.zoomLevel}, ${this.zoomLevel})` `translate(-50%, -50%) scale(${this.zoomLevel}, ${this.zoomLevel})`
); );
// update current settings // update current settings
this.updateUrlParams({ this.updateUrlParams({
zoom: this.zoomMode === "auto" ? undefined : this.zoomLevel, zoom: this.zoomMode === 'auto' ? undefined : this.zoomLevel,
}); });
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "change-zoom", eventAction: 'change-zoom',
eventLabel: "Change Zoom", eventLabel: 'Change Zoom',
eventValue: this.zoomLevel, eventValue: this.zoomLevel,
}); });
} }
@ -983,7 +1059,7 @@ class Gamepad {
// compute next type // compute next type
const types = Object.keys(this.identifiers).filter( const types = Object.keys(this.identifiers).filter(
(i) => i !== "debug" (i) => i !== 'debug'
); );
let typeIndex = types.reduce((typeIndex, type, index) => { let typeIndex = types.reduce((typeIndex, type, index) => {
return type === this.type ? index : typeIndex; return type === this.type ? index : typeIndex;
@ -992,10 +1068,10 @@ class Gamepad {
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "toggle-type", eventAction: 'toggle-type',
eventLabel: "Toggle Type", eventLabel: 'Toggle Type',
eventValue: this.type, eventValue: this.type,
}); });
} }
@ -1019,10 +1095,10 @@ class Gamepad {
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "toggle-debug", eventAction: 'toggle-debug',
eventLabel: "Toggle Debug", eventLabel: 'Toggle Debug',
eventValue: this.debug, eventValue: this.debug,
}); });
} }
@ -1036,19 +1112,20 @@ class Gamepad {
*/ */
toggleHelp() { toggleHelp() {
// refresh gamepad lsit with latest data // refresh gamepad lsit with latest data
this.pollGamepads();
this.buildHelpGamepadList(); this.buildHelpGamepadList();
// display the help popout // display the help popout
this.$helpPopout.toggleClass("active"); this.$helpPopout.toggleClass('active');
this.helpVisible = this.$helpPopout.is(".active"); this.helpVisible = this.$helpPopout.is('.active');
// save statistics // save statistics
if (!!window.ga) { if (!!window.ga) {
ga("send", "event", { ga('send', 'event', {
eventCategory: "Gamepad", eventCategory: 'Gamepad',
eventAction: "toggle-help", eventAction: 'toggle-help',
eventLabel: "Toggle Help", eventLabel: 'Toggle Help',
eventValue: this.$helpPopout.is("active"), eventValue: this.$helpPopout.is('active'),
}); });
} }
} }
@ -1062,12 +1139,12 @@ class Gamepad {
this.triggersMeter = this.triggersMeter =
useMeter !== undefined ? useMeter : !this.triggersMeter; useMeter !== undefined ? useMeter : !this.triggersMeter;
this.$gamepad[this.triggersMeter ? "addClass" : "removeClass"]( this.$gamepad[this.triggersMeter ? 'addClass' : 'removeClass'](
"triggers-meter" 'triggers-meter'
); );
// update current settings // update current settings
const triggers = this.triggersMeter ? "meter" : "opacity"; const triggers = this.triggersMeter ? 'meter' : 'opacity';
this.updateUrlParams({ triggers }); this.updateUrlParams({ triggers });
this.$triggersSelect.val(triggers); this.$triggersSelect.val(triggers);
} }
@ -1076,9 +1153,10 @@ class Gamepad {
* Reads an URL search parameter * Reads an URL search parameter
* *
* @param {*} name * @param {*} name
* @returns {string|boolean|null}
*/ */
getUrlParam(name) { getUrlParam(name) {
let matches = new RegExp("[?&]" + name + "(=([^&#]*))?").exec( let matches = new RegExp('[?&]' + name + '(=([^&#]*))?').exec(
window.location.search window.location.search
); );
return matches ? decodeURIComponent(matches[2] || true) || true : null; return matches ? decodeURIComponent(matches[2] || true) || true : null;
@ -1086,12 +1164,14 @@ class Gamepad {
/** /**
* Read url settings to produce a key/value object * Read url settings to produce a key/value object
*
* @returns {object}
*/ */
getUrlParams() { getUrlParams() {
const settingsArr = window.location.search const settingsArr = window.location.search
.replace("?", "") .replace('?', '')
.split("&") .split('&')
.map((param) => param.split("=")); .map((param) => param.split('='));
const settings = {}; const settings = {};
Object.keys(settingsArr).forEach((key) => { Object.keys(settingsArr).forEach((key) => {
const [k, v] = settingsArr[key]; const [k, v] = settingsArr[key];
@ -1105,6 +1185,7 @@ class Gamepad {
*/ */
clearUrlParams() { clearUrlParams() {
this.updateUrlParams({ this.updateUrlParams({
gamepad: undefined,
type: undefined, type: undefined,
color: undefined, color: undefined,
debug: undefined, debug: undefined,
@ -1123,7 +1204,7 @@ class Gamepad {
const query = Object.entries(params) const query = Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null) .filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join("&"); .join('&');
window.history.replaceState({}, document.title, `/?${query}`); window.history.replaceState({}, document.title, `/?${query}`);
} }
} }

5
samples.txt Normal file
View File

@ -0,0 +1,5 @@
Sample: T818
http://localhost:8081/?gamepad=Thrustmaster%20Thrustmaster%20Advance%20Racer%20(Vendor:%20044f%20Product:%20b696)&triggers=opacity&clutchIndex=6&clutchMin=1&clutchMax=-1&brakeIndex=1&brakeMin=1&brakeMax=-1&throttleIndex=5&throttleMin=1&throttleMax=-1&directionIndex=0&directionDegrees=900&type=telemetry
Sample: Xbox Controller
http://localhost:8081/?gamepad=Microsoft%20Controller%20(STANDARD%20GAMEPAD%20Vendor:%20045e%20Product:%200b00)&triggers=opacity&brakeType=button&brakeIndex=6&brakeMin=0&brakeMax=1&throttleType=button&throttleIndex=7&throttleMin=0&throttleMax=1&directionIndex=0&directionDegrees=180&type=telemetry

View File

@ -17,7 +17,7 @@
$id.html(activeGamepad.id); $id.html(activeGamepad.id);
updateTimestamp(); updateTimestamp();
$index.html(activeGamepad.index); $index.html(activeGamepad.index);
$mapping.html(activeGamepad.mapping); $mapping.html(activeGamepad.mapping || 'N/A');
$rumble.html( $rumble.html(
activeGamepad.vibrationActuator activeGamepad.vibrationActuator
? activeGamepad.vibrationActuator.type ? activeGamepad.vibrationActuator.type

View File

@ -0,0 +1,116 @@
#gamepad #telemetry {
background: var(--main-bg-color);
display: flex;
width: 650px;
height: 120px;
border-top-right-radius: 60px;
border-bottom-right-radius: 60px;
--black-color: black;
--white-color: white;
--grey-color: grey;
--main-bg-color: #444;
--main-component-color: grey;
--meter-idle-color: var(--grey-color);
--clutch-color: blue;
--brake-color: red;
--throttle-color: lime;
}
#gamepad #telemetry #chart {
flex: 1;
margin: 4px;
background-color: var(--main-component-color);
border: 1px solid var(--black-color);
}
#gamepad #telemetry #meters {
display: flex;
justify-content: space-around;
width: 90px;
}
#gamepad #telemetry #meters .meter {
display: flex;
flex-direction: column;
flex: 1;
margin: 4px 2px;
}
#gamepad #telemetry #meters .meter .value {
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 10pt;
color: var(--white-color);
}
#gamepad #telemetry #meters .meter .bar {
display: flex;
align-items: flex-end;
flex: 1;
background-color: var(--main-component-color);
border: 1px solid var(--black-color);
}
#gamepad #telemetry #meters .meter .bar .filler {
width: 100%;
height: 0%;
transition: height 100ms;
}
#gamepad #telemetry #meters #clutch.meter .bar .filler {
background-color: var(--clutch-color);
}
#gamepad #telemetry #meters #brake.meter .bar .filler {
background-color: var(--brake-color);
}
#gamepad #telemetry #meters #throttle.meter .bar .filler {
background-color: var(--throttle-color);
}
#gamepad #telemetry #direction {
display: flex;
justify-content: center;
align-items: center;
width: 120px;
border-radius: 50%;
}
#gamepad #telemetry #direction #wheel {
position: relative;
display: flex;
justify-content: center;
width: 90%;
height: 90%;
border-radius: 50%;
background-color: var(--black-color);
}
#gamepad #telemetry #direction #wheel #wheel--center {
display: block;
position: absolute;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
border-radius: 50%;
background-color: var(--main-bg-color);
}
#gamepad #telemetry #direction #wheel #wheel--indicator {
display: block;
width: 7%;
height: 50%;
background-color: var(--white-color);
transform-origin: bottom;
transform: rotate(0deg);
transition: transform 100ms;
}
#gamepad #telemetry #direction {
display: flex;
justify-content: center;
align-items: center;
width: 120px;
border-radius: 50%;
}

View File

@ -0,0 +1,32 @@
<link rel="stylesheet" href="templates/telemetry/template.css">
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="templates/telemetry/template.js"></script>
<div id="telemetry">
<div id="chart"></div>
<div id="meters">
<div id="clutch" class="meter">
<div class="value">0</div>
<div class="bar">
<div class="filler"></div>
</div>
</div>
<div id="brake" class="meter">
<div class="value">0</div>
<div class="bar">
<div class="filler"></div>
</div>
</div>
<div id="throttle" class="meter">
<div class="value">0</div>
<div class="bar">
<div class="filler"></div>
</div>
</div>
</div>
<div id="direction">
<div id="wheel">
<div id="wheel--indicator"></div>
<div id="wheel--center"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,143 @@
/**
* The Telemetry template class
*
* @class TelemetryTemplate
*/
class TelemetryTemplate {
/**
* Creates an instance of TelemetryTemplate.
*/
constructor() {
this.AXES = ['clutch', 'brake', 'throttle', 'direction'];
this.frequency = 1000 / 60;
this.historyLength = 5000;
this.length = this.historyLength / this.frequency;
this.index = 0;
this.$clutchValue = document.querySelector('#clutch .value');
this.$clutchBar = document.querySelector('#clutch .bar .filler');
this.$brakeValue = document.querySelector('#brake .value');
this.$brakeBar = document.querySelector('#brake .bar .filler');
this.$throttleValue = document.querySelector('#throttle .value');
this.$throttleBar = document.querySelector('#throttle .bar .filler');
this.$directionIndicator = document.querySelector('#direction #wheel--indicator');
this.init();
}
toPercentage(value, min, max) {
// debugger;
return value !== undefined
? Math.round((value - min) * (100 / (max - min)))
: 0;
}
toDegrees(value, min, max) {
const percentage = this.toPercentage(value, min, max);
return (this.directionDegrees) * (percentage - 50) / 100;
}
toAxisValue(gamepad, axis) {
const { [`${axis}Type`]: type, [`${axis}Index`]: index, [`${axis}Min`]: min, [`${axis}Max`]: max } = this[axis];
const value = type === 'button' ? gamepad.buttons[index].value : gamepad.axes[index];
return axis === 'direction' ? this.toDegrees(value, min, max) : this.toPercentage(value, min, max);
}
loadAxes() {
this.AXES.forEach((axis) => {
this[axis] = {
[`${axis}Type`]: (window.gamepad.getUrlParam(`${axis}Type`) || 'axis').replace('axis', 'axe'),
[`${axis}Index`]: window.gamepad.getUrlParam(`${axis}Index`),
[`${axis}Min`]: window.gamepad.getUrlParam(`${axis}Min`) || -1,
[`${axis}Max`]: window.gamepad.getUrlParam(`${axis}Max`) || 1,
}
});
this.directionDegrees = window.gamepad.getUrlParam('directionDegrees') || 360;
}
init() {
this.loadAxes();
if (!window.google) {
window.setTimeout(this.init.bind(this), 100);
return;
}
google.charts.load('current', {
packages: ['corechart', 'line'],
});
google.charts.setOnLoadCallback(this.drawChart.bind(this));
}
drawChart() {
this.initialData = [['Time', 'Clutch', 'Brake', 'Throttle']];
for (this.index = 0; this.index < this.length; this.index++) {
this.initialData.push([this.index, 0, 0, 0]);
}
this.data = google.visualization.arrayToDataTable(this.initialData);
this.options = {
backgroundColor: 'transparent',
chartArea: {
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundColor: 'transparent',
},
hAxis: {
textPosition: 'none',
gridlines: {
color: 'transparent',
}
},
vAxis: {
textPosition: 'none',
gridlines: {
color: 'transparent',
},
minValue: 0,
maxValue: 100,
},
colors: ['blue', 'red', 'lime'],
legend: 'none'
};
this.chart = new google.visualization.LineChart(document.querySelector('#chart'));
this.chart.draw(this.data, this.options);
this.update();
}
update() {
const gamepad = window.gamepad.getActive();
if (!gamepad) return;
const [clutch, brake, throttle, direction] = this.AXES.map((axis) => this.toAxisValue(gamepad, axis));
this.updateChart(clutch, brake, throttle);
this.updateMeters(clutch, brake, throttle, direction);
window.setTimeout(this.update.bind(this), this.frequency);
}
updateChart(clutch, brake, throttle) {
if (this.data.getNumberOfRows() > this.length) {
this.data.removeRows(0, this.data.getNumberOfRows() - this.length);
}
this.data.addRow([this.index, clutch, brake, throttle]);
this.chart.draw(this.data, this.options);
this.index++;
}
updateMeters(clutch, brake, throttle, direction) {
Object.entries({ clutch, brake, throttle, direction }).forEach(([axis, value]) => {
if (axis === 'direction') {
this.$directionIndicator.style.transform = `rotate(${value}deg)`;
return;
}
this[`$${axis}Value`].innerHTML = value;
this[`$${axis}Bar`].style.height = `${value}%`;
});
};
}
window.telemetryTemplate = new TelemetryTemplate();