telemetry wizard

This commit is contained in:
e7d 2023-06-07 00:22:44 +02:00
parent 9f765b671f
commit 0fcb7c1afc
No known key found for this signature in database
GPG Key ID: F320BE007C0B8881
16 changed files with 790 additions and 352 deletions

View File

@ -99,6 +99,11 @@ body.unsupported #gamepad {
}
#gamepad {
width: 100vw;
height: 100vh;
}
#gamepad .controller {
background-position: center;
background-repeat: no-repeat;
background-size: contain;

View File

@ -759,7 +759,7 @@ class Gamepad {
script.src = `templates/${this.type}/template.js`;
script.onload = () => {
// initialize the template
new this.template();
this.template = new this.templateClass();
// enqueue the initial display refresh
this.startTemplate();
@ -1087,7 +1087,7 @@ class Gamepad {
// update the DOM with the zoom value
this.$gamepad.style.setProperty(
'transform',
`translate(-50%, -50%) scale(${this.zoomLevel}, ${this.zoomLevel})`
`scale(${this.zoomLevel}, ${this.zoomLevel})`
);
// update current settings
@ -1127,7 +1127,7 @@ class Gamepad {
*
* @param {boolean|undefined} debug
*/
toggleDebug(debug = null) {
toggleDebug(debug) {
// ensure that a gamepad is currently active
if (this.index === null) return;

View File

@ -1,3 +1,4 @@
<div class="controller">
<div class="info">
<div class="container">
<div id="info-name" class="box extra-large">
@ -60,3 +61,4 @@
<div class="buttons">
<div class="container"></div>
</div>
</div>

View File

@ -1,4 +1,4 @@
window.gamepad.template = class DebugTemplate {
window.gamepad.templateClass = class DebugTemplate {
/**
* Instanciates a new debug template
*/
@ -98,7 +98,7 @@ window.gamepad.template = class DebugTemplate {
* Updates the value of an element
*
* @param {Element} $elem
* @param {Number} precision
* @param {number} precision
*/
updateElem($elem, value, precision = 2) {
this.updateTimestamp();

View File

@ -1,67 +1,67 @@
#gamepad {
.controller {
width: 806px;
height: 598px;
}
#gamepad[data-color="black"] {
#gamepad[data-color="black"] .controller {
background-image: url(base-black.svg);
}
#gamepad[data-color="white"] {
#gamepad[data-color="white"] .controller {
background-image: url(base-white.svg);
}
#gamepad[data-color="red"] {
#gamepad[data-color="red"] .controller {
background-image: url(base-red.svg);
}
#gamepad[data-color="blue"] {
#gamepad[data-color="blue"] .controller {
background-image: url(base-blue.svg);
}
#gamepad.disconnected {
#gamepad.disconnected .controller {
background-image: url(disconnected.svg);
}
#gamepad.disconnected div {
#gamepad.disconnected .controller div {
display: none;
}
#gamepad .triggers {
.controller .triggers {
width: 588px;
height: 90px;
position: absolute;
left: 109px;
}
#gamepad .trigger {
.controller .trigger {
width: 99px;
height: 100%;
background: url(triggers.svg);
clip-path: inset(100% 0px 0px 0pc);
}
#gamepad .trigger[data-value="0"] {
.controller .trigger[data-value="0"] {
opacity: 0;
}
#gamepad .trigger.left {
.controller .trigger.left {
float: left;
}
#gamepad .trigger.right {
.controller .trigger.right {
float: right;
background-position-x: 99px;
}
#gamepad .bumper {
.controller .bumper {
width: 99px;
height: 23px;
background: url(bumper.svg) no-repeat;
opacity: 0;
}
#gamepad .bumpers {
.controller .bumpers {
position: absolute;
width: 588px;
height: 23px;
@ -69,22 +69,22 @@
top: 94px;
}
#gamepad .bumper[data-pressed="true"] {
.controller .bumper[data-pressed="true"] {
opacity: 1;
}
#gamepad .bumper.left {
.controller .bumper.left {
/* -webkit-transform: rotateY(180deg); */
/* transform: rotateY(180deg); */
float: left;
}
#gamepad .bumper.right {
.controller .bumper.right {
float: right;
transform: rotateY(180deg);
}
#gamepad .touchpad {
.controller .touchpad {
width: 262px;
height: 151px;
position: absolute;
@ -92,11 +92,11 @@
top: 122px;
}
#gamepad .touchpad[data-pressed="true"] {
.controller .touchpad[data-pressed="true"] {
background: url(touchpad.svg) no-repeat center;
}
#gamepad .meta {
.controller .meta {
width: 42px;
height: 42px;
position: absolute;
@ -104,11 +104,11 @@
bottom: 216px;
}
#gamepad .meta[data-pressed="true"] {
.controller .meta[data-pressed="true"] {
background: url(meta.svg) no-repeat center;
}
#gamepad .arrows {
.controller .arrows {
position: absolute;
width: 352px;
height: 46px;
@ -116,29 +116,29 @@
left: 227px;
}
#gamepad .select,
#gamepad .start {
.controller .select,
.controller .start {
background: url(start.svg);
width: 28px;
height: 46px;
opacity: 0;
}
#gamepad .select[data-pressed="true"],
#gamepad .start[data-pressed="true"] {
.controller .select[data-pressed="true"],
.controller .start[data-pressed="true"] {
opacity: 1;
}
#gamepad .select {
.controller .select {
float: left;
}
#gamepad .start {
.controller .start {
float: right;
background-position: 28px 0;
}
#gamepad .buttons {
.controller .buttons {
position: absolute;
width: 170px;
height: 170px;
@ -146,40 +146,40 @@
left: 567px;
}
#gamepad .button {
.controller .button {
position: absolute;
width: 56px;
height: 56px;
background: url(buttons.svg);
}
#gamepad .button[data-pressed="true"] {
.controller .button[data-pressed="true"] {
background-position-y: 56px;
}
#gamepad .a {
.controller .a {
background-position: 0 0;
bottom: 0px;
left: 56px;
}
#gamepad .b {
.controller .b {
background-position: -56px 0;
top: 56px;
right: 0px;
}
#gamepad .x {
.controller .x {
background-position: 112px 0;
top: 56px;
}
#gamepad .y {
.controller .y {
background-position: 56px 0;
left: 56px;
}
#gamepad .sticks {
.controller .sticks {
position: absolute;
width: 361px;
height: 105px;
@ -187,32 +187,32 @@
left: 228px;
}
#gamepad .stick {
.controller .stick {
position: absolute;
background: url(sticks.svg);
height: 94px;
width: 94px;
}
#gamepad .stick[data-pressed="true"].left {
.controller .stick[data-pressed="true"].left {
background-position-x: -96px;
}
#gamepad .stick[data-pressed="true"].right {
.controller .stick[data-pressed="true"].right {
background-position-x: -192px;
}
#gamepad .stick.left {
.controller .stick.left {
top: 0;
left: 0;
}
#gamepad .stick.right {
.controller .stick.right {
top: calc(100% - 105px);
left: calc(100% - 105px);
}
#gamepad .dpad {
.controller .dpad {
position: absolute;
width: 125px;
height: 126px;
@ -220,52 +220,52 @@
left: 92px;
}
#gamepad .face {
.controller .face {
background: url(dpad.svg);
position: absolute;
}
#gamepad .face.up,
#gamepad .face.down {
.controller .face.up,
.controller .face.down {
width: 36px;
height: 52px;
}
#gamepad .face.left,
#gamepad .face.right {
.controller .face.left,
.controller .face.right {
width: 52px;
height: 36px;
}
#gamepad .face.up {
.controller .face.up {
left: 44px;
top: 0;
background-position: -37px 0px;
}
#gamepad .face.down {
.controller .face.down {
left: 44px;
bottom: 0;
background-position: 0px 0;
}
#gamepad .face.left {
.controller .face.left {
top: 44px;
left: 0;
background-position: 104px 0;
}
#gamepad .face.right {
.controller .face.right {
top: 44px;
right: 0px;
background-position: 52px 0;
}
#gamepad .face[data-pressed="true"] {
.controller .face[data-pressed="true"] {
/* margin-top: 5px; */
background-position-y: 52px;
}
#gamepad.half {
.controller.half {
margin-top: -300px;
}

View File

@ -1,3 +1,4 @@
<div class="controller">
<div class="triggers">
<span class="trigger left" data-button="6"></span>
<span class="trigger right" data-button="7"></span>
@ -31,3 +32,4 @@
<span class="face left" data-button="14"></span>
<span class="face right" data-button="15"></span>
</div>
</div>

View File

@ -1,4 +1,4 @@
window.gamepad.template = class DualShock4Template {
window.gamepad.templateClass = class DualShock4Template {
/**
* Instanciates a new DualShock 4 controller template
*/

View File

@ -1,17 +1,17 @@
#gamepad {
.controller {
height: 700px;
width: 1200px;
}
#gamepad[data-color="black"] {
#gamepad[data-color="black"] .controller {
background-image: url(base-black.png);
}
#gamepad[data-color="white"] {
#gamepad[data-color="white"] .controller {
background-image: url(base-white.png);
}
#gamepad.disconnected div {
#gamepad.disconnected .controller {
display: none;
}

View File

@ -1,3 +1,4 @@
<div class="controller">
<div class="triggers">
<span class="trigger left" data-button="6"></span>
<span class="trigger right" data-button="7"></span>
@ -31,3 +32,4 @@
<span class="face left" data-button="14"></span>
<span class="face right" data-button="15"></span>
</div>
</div>

View File

@ -1,4 +1,4 @@
window.gamepad.template = class DualSenseTemplate {
window.gamepad.templateClass = class DualSenseTemplate {
/**
* Instanciates a new DualSense controller template
*/

View File

@ -1,12 +1,9 @@
#gamepad #telemetry {
background: var(--main-bg-color);
display: flex;
width: 650px;
height: 120px;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
border-top-right-radius: 60px;
border-bottom-right-radius: 60px;
padding: 4px;
border-radius: 6px;
--black-color: black;
--white-color: white;
@ -17,13 +14,23 @@
--throttle-color: #0CA818;
}
#gamepad #telemetry.with-steering {
border-top-right-radius: 60px;
border-bottom-right-radius: 60px;
}
#gamepad #telemetry,
#gamepad #telemetry * {
box-sizing: border-box;
}
#gamepad #telemetry>*+* {
margin-left: 2px;
}
#gamepad #telemetry #chart {
flex: 1;
margin: 4px;
width: 400px;
background-color: var(--main-component-color);
border: 1px solid var(--black-color);
border-radius: 4px;
@ -32,17 +39,20 @@
#gamepad #telemetry #meters {
display: flex;
justify-content: space-around;
width: 90px;
}
#gamepad #telemetry #meters .meter {
position: relative;
flex: 1;
margin: 4px 2px;
background-color: var(--main-component-color);
border: 1px solid var(--black-color);
border-radius: 4px;
overflow: hidden;
width: 30px;
}
#gamepad #telemetry #meters .meter+.meter {
margin-left: 2px;
}
#gamepad #telemetry #meters .meter .value {
@ -62,28 +72,31 @@
height: 0%;
transition: height 100ms;
}
#gamepad #telemetry #meters #clutch.meter .bar {
background-color: var(--clutch-color);
}
#gamepad #telemetry #meters #brake.meter .bar {
background-color: var(--brake-color);
}
#gamepad #telemetry #meters #throttle.meter .bar {
background-color: var(--throttle-color);
}
#gamepad #telemetry #direction {
#gamepad #telemetry #steering {
position: relative;
display: flex;
justify-content: center;
width: 100px;
height: 100px;
margin: 10px;
margin: 6px;
border-radius: 50px;
background-color: var(--main-component-color);
}
#gamepad #telemetry #direction .center {
#gamepad #telemetry #steering .center {
position: absolute;
top: 20%;
left: 20%;
@ -93,7 +106,7 @@
background-color: var(--main-bg-color);
}
#gamepad #telemetry #direction .indicator {
#gamepad #telemetry #steering .indicator {
display: block;
width: 5%;
height: 50%;
@ -101,3 +114,36 @@
transform-origin: bottom;
transition: transform 100ms;
}
#wizard #wizard-instructions h4,
#wizard #wizard-instructions p {
text-align: center;
}
#wizard #wizard-instructions h4 {
margin-bottom: 0.5em;
}
#wizard .wizard-options {
display: flex;
margin: auto;
justify-content: center;
}
#wizard .wizard-options div {
margin: 0 8px;
line-height: 1.6em;
}
#wizard .wizard-options div label,
#wizard .wizard-options div input {
cursor: pointer;
}
#wizard .wizard-options input[name="history-length-option"] {
width: 35px;
}
#wizard .wizard-options input[name="steering-angle-option"] {
width: 50px;
}

View File

@ -1,5 +1,4 @@
<div id="fps"></div>
<div id="telemetry">
<div id="telemetry" class="controller">
<div id="chart"></div>
<div id="meters">
<div id="clutch" class="meter">
@ -15,8 +14,14 @@
<div class="value">0</div>
</div>
</div>
<div id="direction">
<div id="steering">
<div class="indicator"></div>
<div class="center"></div>
</div>
</div>
<div id="wizard" class="popout">
<div class="content">
<h2>Telemetry Wizard</h2>
<div id="wizard-instructions"></div>
</div>
</div>

View File

@ -1,9 +1,19 @@
window.gamepad.template = class TelemetryTemplate {
gamepad.templateClass = class TelemetryTemplate {
/**
* Instanciates a new telemetry template
*/
constructor() {
this.AXES = ['clutch', 'brake', 'throttle', 'steering'];
this.gamepad = window.gamepad;
this.loadSelectors();
this.loadUrlParams();
if (!this.AXES.some((axis) => this[axis].index)) {
this.wizard();
return;
}
this.init();
}
@ -17,28 +27,28 @@ window.gamepad.template = class TelemetryTemplate {
/**
* Converts a value to a percentage
*
* @param {Number} value
* @param {Number} min
* @param {Number} max
* @returns {Number}
* @param {number} value
* @param {number} min
* @param {number} max
* @returns {number}
*/
toPercentage(value, min, max) {
return value !== undefined
? Math.round((value - min) * (100 / (max - min)))
? Math.max(0, Math.min(100, Math.round((value - min) * (100 / (max - min)))))
: 0;
}
/**
* Converts a value to degrees
*
* @param {Number} value
* @param {Number} min
* @param {Number} max
* @returns {Number}
* @param {number} value
* @param {number} min
* @param {number} max
* @returns {number}
*/
toDegrees(value, min, max) {
const percentage = this.toPercentage(value, min, max);
return (this.directionDegrees) * (percentage - 50) / 100;
return (this.angle) * (percentage - 50) / 100;
}
/**
@ -46,85 +56,128 @@ window.gamepad.template = class TelemetryTemplate {
*
* @param {object} gamepad
* @param {string} axis
* @returns {Number}
* @returns {number}
*/
toAxisValue(gamepad, axis) {
const { [`${axis}Type`]: type, [`${axis}Index`]: index, [`${axis}Min`]: min, [`${axis}Max`]: max } = this[axis];
const { type, index, min, max } = this[axis];
if (!type || !index) return null;
const value = type === 'button' ? gamepad.buttons[index].value : gamepad.axes[index];
return axis === 'direction' ? this.toDegrees(value, min, max) : this.toPercentage(value, min, max);
return axis === 'steering' ? this.toDegrees(value, min, max) : this.toPercentage(value, min, max);
}
/**
* Loads the axes
* Loads the DOM selectors
*/
loadAxes() {
loadSelectors() {
this.$telemetry = document.querySelector('#telemetry');
this.$chart = this.$telemetry.querySelector('#chart');
this.$meters = this.$telemetry.querySelector('#meters');
this.$clutch = this.$telemetry.querySelector('#clutch');
this.$clutchBar = this.$clutch.querySelector('.bar');
this.$clutchValue = this.$clutch.querySelector('.value');
this.$brake = this.$telemetry.querySelector('#brake');
this.$brakeBar = this.$brake.querySelector('.bar');
this.$brakeValue = this.$brake.querySelector('.value');
this.$throttle = this.$telemetry.querySelector('#throttle');
this.$throttleBar = this.$throttle.querySelector('.bar');
this.$throttleValue = this.$throttle.querySelector('.value');
this.$steering = this.$telemetry.querySelector('#steering');
this.$steeringIndicator = this.$steering.querySelector('.indicator');
this.$wizard = document.querySelector('#wizard');
this.$wizardInstructions = this.$wizard.querySelector('#wizard-instructions');
}
/**
* Loads the params from the URL
*/
loadUrlParams() {
this.withChart = gamepad.getUrlParam('chart') !== 'false';
this.historyLength = gamepad.getUrlParam('history') || 5000;
this.withMeters = gamepad.getUrlParam('meters') !== 'false';
this.withSteering = gamepad.getUrlParam('steeringIndex') !== null;
this.angle = gamepad.getUrlParam('angle') || 360;
this.frequency = gamepad.getUrlParam('fps') || 60;
this.AXES.forEach((axis) => {
this[axis] = {
[`${axis}Type`]: (gamepad.getUrlParam(`${axis}Type`) || 'axis').replace('axis', 'axe'),
[`${axis}Index`]: gamepad.getUrlParam(`${axis}Index`),
[`${axis}Min`]: gamepad.getUrlParam(`${axis}Min`) || -1,
[`${axis}Max`]: gamepad.getUrlParam(`${axis}Max`) || 1,
type: (gamepad.getUrlParam(`${axis}Type`) || 'axis').replace('axis', 'axe'),
index: gamepad.getUrlParam(`${axis}Index`),
min: gamepad.getUrlParam(`${axis}Min`) || -1,
max: gamepad.getUrlParam(`${axis}Max`) || 1,
}
});
this.directionDegrees = gamepad.getUrlParam('directionDegrees') || 360;
}
/**
* Initializes the template
* Sets up the template
*/
init() {
this.$fps = document.querySelector('#fps');
this.$clutchBar = document.querySelector('#clutch .bar');
this.$clutchValue = document.querySelector('#clutch .value');
this.$brakeBar = document.querySelector('#brake .bar');
this.$brakeValue = document.querySelector('#brake .value');
this.$throttleBar = document.querySelector('#throttle .bar');
this.$throttleValue = document.querySelector('#throttle .value');
this.$directionIndicator = document.querySelector('#direction .indicator');
this.AXES = ['clutch', 'brake', 'throttle', 'direction'];
this.frequency = gamepad.getUrlParam('fps') || 60;
this.historyLength = gamepad.getUrlParam('history') || 5000;
this.index = 0;
this.interval = 1000 / this.frequency;
this.length = this.historyLength / this.interval;
this.loadAxes();
this.initChart();
setupTemplate() {
if (!this.withChart) this.$chart.remove();
if (!this.clutch.index) this.$clutch.remove();
if (!this.brake.index) this.$brake.remove();
if (!this.throttle.index) this.$throttle.remove();
if (!this.withMeters) this.$meters.remove();
if (!this.steering.index) {
this.$steering.remove();
} else {
this.$telemetry.classList.add('with-steering');
}
}
/**
* Initializes the live chart
*/
initChart() {
async setupChart() {
if (!this.withChart) return;
return new Promise((resolve) => {
const script = document.createElement('script');
script.async = true;
script.src = `https://www.gstatic.com/charts/loader.js`;
script.onload = () => {
if (!google || !google.visualization) {
this.loadGoogleCharts();
this.loadGoogleCharts(resolve);
return;
}
this.drawChart();
this.drawChart(resolve);
};
this.gamepad.$gamepad.appendChild(script);
});
}
/**
* Initializes the live chart
*/
async init() {
this.interval = 1000 / this.frequency;
this.length = this.historyLength / this.interval;
this.setupTemplate();
await this.setupChart();
this.running = true;
this.update();
}
/**
* Loads the Google Charts library
*/
loadGoogleCharts() {
loadGoogleCharts(resolve) {
google.charts.load('current', { packages: ['corechart', 'line'], });
google.charts.setOnLoadCallback(this.drawChart.bind(this));
google.charts.setOnLoadCallback(this.drawChart.bind(this, resolve));
}
/**
* Draws the live chart with the initial data
* Draws the live chart with the initial data and starts the draw update loop
*/
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]);
drawChart(resolve) {
const now = Date.now();
const initialData = [['time', 'clutch', 'brake', 'throttle']];
for (let index = now - this.historyLength; index < now; index += this.interval) {
initialData.push([index, 0, 0, 0]);
}
this.data = google.visualization.arrayToDataTable(this.initialData);
this.data = google.visualization.arrayToDataTable(initialData);
this.options = {
backgroundColor: 'transparent',
chartArea: {
@ -138,6 +191,10 @@ window.gamepad.template = class TelemetryTemplate {
textPosition: 'none',
gridlines: {
color: 'transparent',
},
viewWindow: {
min: now - this.historyLength,
max: now
}
},
vAxis: {
@ -153,13 +210,14 @@ window.gamepad.template = class TelemetryTemplate {
}
},
colors: ['#2D64B9', '#A52725', '#0CA818'],
legend: 'none'
legend: 'none',
tooltip: {
trigger: 'none'
}
};
this.chart = new google.visualization.LineChart(document.querySelector('#chart'));
this.chart.draw(this.data, this.options);
this.running = true;
this.update();
resolve();
}
/**
@ -168,11 +226,11 @@ window.gamepad.template = class TelemetryTemplate {
update() {
if (!this.running) return;
const activeGamepad = this.gamepad.getActive();
if (!activeGamepad) return;
const [clutch, brake, throttle, direction] = this.AXES.map((axis) => this.toAxisValue(activeGamepad, axis));
this.updateChart(clutch, brake, throttle);
this.updateMeters(clutch, brake, throttle, direction);
const gamepad = this.gamepad.getActive();
const [clutch, brake, throttle, steering] = this.AXES.map((axis) => this.toAxisValue(gamepad, axis));
if (this.withChart) this.updateChart(clutch, brake, throttle);
if (this.withMeters) this.updateMeters(clutch, brake, throttle);
if (this.withSteering) this.updateSteering(steering);
window.setTimeout(this.update.bind(this), this.interval);
}
@ -180,34 +238,350 @@ window.gamepad.template = class TelemetryTemplate {
/**
* Updates the live chart with the latest data
*
* @param {Number} clutch
* @param {Number} brake
* @param {Number} throttle
* @param {number} clutch
* @param {number} brake
* @param {number} throttle
*/
updateChart(clutch, brake, throttle) {
this.data.removeRows(0, 1);
this.data.addRow([this.index, clutch, brake, throttle]);
const now = Date.now();
this.data.removeRows(0, this.data.getFilteredRows([{ column: 0, maxValue: now - this.historyLength }]).length);
this.data.addRow([now, clutch, brake, throttle]);
this.options.hAxis.viewWindow = {
min: now - this.historyLength,
max: now
};
this.chart.draw(this.data, this.options);
this.index++;
}
/**
* Updates the meters with the latest data
*
* @param {Number} clutch
* @param {Number} brake
* @param {Number} throttle
* @param {Number} direction
* @param {number} clutch
* @param {number} brake
* @param {number} throttle
* @param {number} steering
*/
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;
}
updateMeters(clutch, brake, throttle) {
Object.entries({ clutch, brake, throttle }).forEach(([axis, value]) => {
if (value === null) return;
this[`$${axis}Value`].innerHTML = value;
this[`$${axis}Value`].style.opacity = `${Math.round(33 + (value / 1.5))}%`;
this[`$${axis}Bar`].style.height = `${value}%`;
});
}
/**
* Updates the steering indicator with the latest data
*
* @param {number} steering
*/
updateSteering(steering) {
this.$steeringIndicator.style.transform = `rotate(${steering}deg)`;
}
/**
* Waits for one or all buttons of a gamepad to be released
*
* @param {number} [index]
* @returns {Promise}
*/
async waitButtonRelease(index = undefined) {
return new Promise((resolve) => {
const interval = window.setInterval(() => {
const gamepad = this.gamepad.getActive();
const pressedButton = index !== undefined
? gamepad.buttons[index].pressed
: gamepad.buttons.some((button) => button.pressed);
if (pressedButton) return;
window.clearInterval(interval);
resolve();
}, 100);
});
}
/**
* Waits for a button to be pressed, then released
*
* @returns {Promise}
*/
async waitButtonClick() {
return new Promise((resolve) => {
const pressInterval = window.setInterval(() => {
const gamepad = this.gamepad.getActive();
const index = gamepad.buttons.findIndex((button) => button.pressed);
if (index === -1) return;
window.clearInterval(pressInterval);
const releaseInterval = window.setInterval(() => {
const gamepad = this.gamepad.getActive();
if (gamepad.buttons[index].pressed) return;
window.clearInterval(releaseInterval);
resolve(index);
}, 100);
}, 100);
});
}
/**
* Waits for an axis to be pushed out, and of the reference value, if any
*
* @param {number} index
* @param {number} [referenceValue]
* @param {number} [duration]
* @returns {Promise}
*/
async getAxisPush(index, referenceValue = undefined, duration = 1000) {
let start;
let value;
return new Promise((resolve) => {
const interval = window.setInterval(() => {
const gamepad = this.gamepad.getActive();
if (start === undefined) {
start = Date.now();
value = gamepad.axes[index];
return;
}
if (referenceValue !== undefined && Math.abs(gamepad.axes[index] - referenceValue) < 0.5) {
}
if (Math.abs(gamepad.axes[index] - value) > 0.05) {
start = undefined;
value = undefined;
return;
}
if (Date.now() - start < duration) {
value = gamepad.axes[index];
return;
}
window.clearInterval(interval);
resolve(value);
}, 100);
});
}
/**
* Capitalizes a word
*
* @param {string} word
* @returns {string}
*/
capitalize(word) {
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
}
/**
* Asks the user for the telemetry options
*
* @returns {Promise}
*/
async askForOptions() {
this.$wizardInstructions.innerHTML = `
<h4>Inputs</h4>
<div class="wizard-options">
<div>
<input id="clutch-option" name="clutch-option" type="checkbox" checked>
<label for="clutch-option">Clutch</label>
</div>
<div>
<input id="brake-option" name="brake-option" type="checkbox" readonly checked>
<label for="brake-option">Brake</label>
</div>
<div>
<input id="throttle-option" name="throttle-option" type="checkbox" checked>
<label for="throttle-option">Throttle</label>
</div>
<div>
<input id="steering-option" name="steering-option" type="checkbox" checked>
<label for="steering-option">Steering</label>
</div>
</div>
<h4>Chart widget</h4>
<div class="wizard-options">
<div>
<input id="chart-option" name="chart-option" type="checkbox" checked>
<label for="chart-option">Enable</label>
</div>
<div>
<label for="history-option">History</label>
<input id="history-option" name="history-option" type="number" min="1" max="30" step="1" value="5">s
</div>
</div>
<h4>Meters widget</h4>
<div class="wizard-options">
<div>
<input id="meters-option" name="meters-option" type="checkbox" checked>
<label for="meters-option">Enable</label>
</div>
</div>
<h4>Steering widget</h4>
<div class="wizard-options">
<div>
<label for="steering-angle-option">Angle</label>
<input id="steering-angle-option" name="steering-angle-option" type="number" min="180" max="1080" step="10" value="360">°
</div>
</div>
<h4>Display mode</h4><div class="wizard-options">
<div>
<label for="fps-option">Mode</label>
<select id="fps-option" name="fps-option">
<option value="30">30 FPS (performance)</option>
<option value="60" selected>60 FPS (quality)</option>
</select>
</div>
</div>
<p>Then, press any button to continue.</p>
`;
await this.waitButtonRelease();
await this.waitButtonClick();
return {
...this.AXES.reduce((options, axis) => {
options[`with${this.capitalize(axis)}`] = document.querySelector(`[name=${axis}-option]`).checked;
return options;
}, {}),
chart: document.querySelector('[name=chart-option]').checked,
history: parseInt(document.querySelector('[name=history-option]').value) * 1000,
meters: document.querySelector('[name=meters-option]').checked,
angle: this.$wizardInstructions.querySelector('input[name="steering-angle-option"]').value,
fps: parseInt(document.querySelector('[name=fps-option]').value)
};
}
/**
* Detects activity on any button or axis of the gamepad
*
* @param {string} [type='button'|'axis'|undefined]
* @param {number} [distance=0.5]
* @returns {Promise}
*/
async detectActivity(type = undefined, distance = 0.5) {
const before = this.gamepad.getActive();
return new Promise((resolve) => {
const interval = window.setInterval(async () => {
const gamepad = this.gamepad.getActive();
const buttonIndex = ['button', undefined].includes(type)
? gamepad.buttons.findIndex((button, index) => button.pressed && Math.abs(button.value - before.buttons[index].value) > distance)
: -1;
const axisIndex = ['axis', undefined].includes(type)
? gamepad.axes.findIndex((axis, index) => Math.abs(axis - before.axes[index]) > distance)
: -1;
if (buttonIndex === -1 && axisIndex === -1) return;
window.clearInterval(interval);
resolve({
type: buttonIndex === -1 ? 'axis' : 'button',
index: buttonIndex === -1 ? axisIndex : buttonIndex,
});
}, 100);
});
}
/**
* Calibrates a pedal
*
* @param {string} name
* @returns {Promise}
*/
async calibratePedal(name) {
this.$wizardInstructions.innerHTML = `
<p>Waiting for <strong>${name}</strong> activity.</p>
`;
const { type, index } = await this.detectActivity();
if (type === 'button') {
this.$wizardInstructions.innerHTML = `
<p>Release the <strong>${name}</strong> button.</p>
`;
await this.waitButtonRelease(index);
return { type, index, releasedValue: 0, pressedValue: 1 };
}
this.$wizardInstructions.innerHTML = `
<p>Press and hold the <strong>${name}</strong> axis.</p>
`;
const pressedValue = await this.getAxisPush(index);
this.$wizardInstructions.innerHTML = `
<p>Release the <strong>${name}</strong> axis.</p>
`;
const releasedValue = await this.getAxisPush(index, pressedValue);
return { type, index, releasedValue, pressedValue };
}
/**
* Calibrates the steering axis
*
* @returns {Promise}
*/
async calibrateSteering() {
this.$wizardInstructions.innerHTML = `
<p>Turn the <strong>steering</strong> axis all the way to the <strong>left</strong>.</p>
`;
const { index } = await this.detectActivity('axis', 0.2);
const leftValue = await this.getAxisPush(index, 0);
this.$wizardInstructions.innerHTML = `
<p>Turn the <strong>steering</strong> axis all the way to the <strong>right</strong>.</p>
`;
const rightValue = await this.getAxisPush(index, leftValue);
return { index, leftValue, rightValue };
}
/**
* Converts an object to a query string, ignoring empty values
*
* @param {Object} options
* @returns {string}
*/
toOptionsParams(options) {
return Object.entries(options)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`)
.join('&');
}
/**
* Generates the query string for a pedal
*
* @param {string} name
* @param {Object} data
* @returns {string}
*/
toPedalParams(name, { type, index, releasedValue, pressedValue }) {
return `${name}Type=${type}&${name}Index=${index}&${name}Min=${releasedValue}&${name}Max=${pressedValue}`;
}
/**
* Generates the query string for the steering
*
* @param {Object} data
* @returns {string}
*/
toSteeringParams({ index, leftValue, rightValue }) {
return `steeringIndex=${index}&steeringMin=${leftValue}&steeringMax=${rightValue}`;
}
/**
* Starts the wizard
*
* @returns {Promise}
*/
async wizard() {
this.$wizard.classList.add('active');
const gamepad = this.gamepad.getActive();
const { withClutch, withBrake, withThrottle, withSteering, chart, history, meters, angle, fps } = await this.askForOptions();
const clutch = withClutch && await this.calibratePedal('clutch');
const brake = withBrake && await this.calibratePedal('brake');
const throttle = withThrottle && await this.calibratePedal('throttle');
const steering = withSteering && await this.calibrateSteering();
window.location.href = [
`?gamepad=${gamepad.id}&type=telemetry`,
this.toOptionsParams({ chart, history, meters, angle, fps }),
withClutch ? this.toPedalParams('clutch', clutch) : null,
withBrake ? this.toPedalParams('brake', brake) : null,
withThrottle ? this.toPedalParams('throttle', throttle) : null,
withSteering ? this.toSteeringParams(steering) : null
].filter(e => e !== null).join('&');
}
};

View File

@ -1,17 +1,17 @@
#gamepad {
.controller {
height: 630px;
width: 750px;
}
#gamepad[data-color="black"] {
#gamepad[data-color="black"] .controller {
background-image: url(base-black.svg);
}
#gamepad[data-color="white"] {
#gamepad[data-color="white"] .controller {
background-image: url(base-white.svg);
}
#gamepad.disconnected {
#gamepad.disconnected .controller {
background-image: url(disconnected.svg);
}
@ -19,42 +19,42 @@
display: none;
}
#gamepad .triggers {
.controller .triggers {
width: 448px;
height: 122px;
position: absolute;
left: 151px;
}
#gamepad .trigger {
.controller .trigger {
width: 89px;
height: 122px;
background: url(trigger.svg);
clip-path: inset(100% 0px 0px 0pc);
}
#gamepad .trigger[data-value="0"] {
.controller .trigger[data-value="0"] {
opacity: 0;
}
#gamepad .trigger.left {
.controller .trigger.left {
float: left;
background-position: 0 0;
}
#gamepad .trigger.right {
.controller .trigger.right {
float: right;
transform: rotateY(180deg);
}
#gamepad .bumper {
.controller .bumper {
width: 170px;
height: 61px;
background: url(bumper.svg);
opacity: 0;
}
#gamepad .bumpers {
.controller .bumpers {
position: absolute;
width: 536px;
height: 61px;
@ -62,41 +62,41 @@
top: 129px;
}
#gamepad .bumper[data-pressed="true"] {
.controller .bumper[data-pressed="true"] {
opacity: 1;
}
#gamepad .bumper.left {
.controller .bumper.left {
float: left;
}
#gamepad .bumper.right {
.controller .bumper.right {
float: right;
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
#gamepad .p0 {
.controller .p0 {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
#gamepad .p1 {
.controller .p1 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
#gamepad .p2 {
.controller .p2 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
#gamepad .p3 {
.controller .p3 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
#gamepad .arrows {
.controller .arrows {
position: absolute;
width: 141px;
height: 33px;
@ -104,29 +104,29 @@
left: 306px;
}
#gamepad .select,
#gamepad .start {
.controller .select,
.controller .start {
background: url(start-select.svg);
width: 33px;
height: 33px;
opacity: 0;
}
#gamepad .select[data-pressed="true"],
#gamepad .start[data-pressed="true"] {
.controller .select[data-pressed="true"],
.controller .start[data-pressed="true"] {
opacity: 1;
}
#gamepad .select {
.controller .select {
float: left;
}
#gamepad .start {
.controller .start {
background-position: 33px 0px;
float: right;
}
#gamepad .buttons {
.controller .buttons {
position: absolute;
width: 155px;
height: 156px;
@ -134,43 +134,43 @@
left: 489px;
}
#gamepad .button {
.controller .button {
position: absolute;
background: url(buttons.svg);
width: 53px;
height: 53px;
}
#gamepad .button[data-pressed="true"] {
.controller .button[data-pressed="true"] {
background-position-y: -53px;
opacity: 1;
}
#gamepad .a {
.controller .a {
background-position: 0 0;
top: 102px;
left: 51px;
}
#gamepad .b {
.controller .b {
background-position: -53px 0;
top: 52px;
right: 1px;
}
#gamepad .x {
.controller .x {
background-position: -106px 0;
top: 52px;
left: 1px;
}
#gamepad .y {
.controller .y {
background-position: -159px 0;
top: 1px;
left: 51px;
}
#gamepad .sticks {
.controller .sticks {
position: absolute;
width: 371px;
height: 196px;
@ -178,7 +178,7 @@
left: 144px;
}
#gamepad .stick {
.controller .stick {
position: absolute;
background: url(stick.svg);
background-position: -85px 0;
@ -186,21 +186,21 @@
width: 83px;
}
#gamepad .stick[data-pressed="true"] {
.controller .stick[data-pressed="true"] {
background-position: 0 0;
}
#gamepad .stick.left {
.controller .stick.left {
top: 0;
left: 0;
}
#gamepad .stick.right {
.controller .stick.right {
top: 113px;
left: 288px;
}
#gamepad .dpad {
.controller .dpad {
position: absolute;
width: 110px;
height: 111px;
@ -208,17 +208,17 @@
left: 223px;
}
#gamepad .face {
.controller .face {
background: url(dpad.svg);
position: absolute;
opacity: 0;
}
#gamepad .face[data-pressed="true"] {
.controller .face[data-pressed="true"] {
opacity: 1;
}
#gamepad .face.up {
.controller .face.up {
background-position: 35px 0;
left: 38px;
top: 1px;
@ -226,14 +226,14 @@
height: 56px;
}
#gamepad .face.down {
.controller .face.down {
left: 38px;
bottom: 0;
width: 34px;
height: 56px;
}
#gamepad .face.left {
.controller .face.left {
background-position: 0 -93px;
width: 56px;
height: 34px;
@ -241,7 +241,7 @@
left: 0;
}
#gamepad .face.right {
.controller .face.right {
background-position: 0 -57px;
width: 56px;
height: 34px;

View File

@ -1,3 +1,4 @@
<div class="controller">
<div class="triggers">
<span class="trigger left" data-button="6"></span>
<span class="trigger right" data-button="7"></span>
@ -30,3 +31,4 @@
<span class="face left" data-button="14"></span>
<span class="face right" data-button="15"></span>
</div>
</div>

View File

@ -1,4 +1,4 @@
window.gamepad.template = class XboxOneTemplate {
window.gamepad.templateClass = class XboxOneTemplate {
/**
* Instanciates a new Xbox One controller template
*/