diff --git a/css/gamepad.css b/css/gamepad.css index 68e47de..2c9c84f 100644 --- a/css/gamepad.css +++ b/css/gamepad.css @@ -99,6 +99,11 @@ body.unsupported #gamepad { } #gamepad { + width: 100vw; + height: 100vh; +} + +#gamepad .controller { background-position: center; background-repeat: no-repeat; background-size: contain; diff --git a/js/gamepad.js b/js/gamepad.js index 544fc43..6ba7b13 100644 --- a/js/gamepad.js +++ b/js/gamepad.js @@ -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; diff --git a/templates/debug/template.html b/templates/debug/template.html index a596dbc..36129fe 100644 --- a/templates/debug/template.html +++ b/templates/debug/template.html @@ -1,62 +1,64 @@ -
-
-
-
-
Name
-
+
+
+
+
+
+
Name
+
+
-
-
-
-
Vendor
-
+
+
+
Vendor
+
+
-
-
-
-
Product
-
+
+
+
Product
+
+
-
-
-
-
ID
-
+
+
+
ID
+
+
-
-
-
-
-
-
Timestamp
-
+
+
+
+
+
Timestamp
+
+
-
-
-
-
Index
-
+
+
+
Index
+
+
-
-
-
-
Mapping
-
+
+
+
Mapping
+
+
-
-
-
-
Vibration
-
+
+
+
Vibration
+
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/templates/debug/template.js b/templates/debug/template.js index 218ce8a..131a47b 100644 --- a/templates/debug/template.js +++ b/templates/debug/template.js @@ -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(); diff --git a/templates/ds4/template.css b/templates/ds4/template.css index f0ced9e..b0dbaae 100644 --- a/templates/ds4/template.css +++ b/templates/ds4/template.css @@ -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; } diff --git a/templates/ds4/template.html b/templates/ds4/template.html index 70ec9fc..3760646 100644 --- a/templates/ds4/template.html +++ b/templates/ds4/template.html @@ -1,33 +1,35 @@ -
- - - -
-
- - - -
-
-
-
- - - -
-
- - - - -
-
- - -
-
- - - - +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ + + + +
+
+ + +
+
+ + + + +
diff --git a/templates/ds4/template.js b/templates/ds4/template.js index 741ffd3..e17c1b4 100644 --- a/templates/ds4/template.js +++ b/templates/ds4/template.js @@ -1,4 +1,4 @@ -window.gamepad.template = class DualShock4Template { +window.gamepad.templateClass = class DualShock4Template { /** * Instanciates a new DualShock 4 controller template */ diff --git a/templates/dualsense/template.css b/templates/dualsense/template.css index 5c11fef..a665080 100644 --- a/templates/dualsense/template.css +++ b/templates/dualsense/template.css @@ -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; } diff --git a/templates/dualsense/template.html b/templates/dualsense/template.html index 70ec9fc..3760646 100644 --- a/templates/dualsense/template.html +++ b/templates/dualsense/template.html @@ -1,33 +1,35 @@ -
- - - -
-
- - - -
-
-
-
- - - -
-
- - - - -
-
- - -
-
- - - - +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ + + + +
+
+ + +
+
+ + + + +
diff --git a/templates/dualsense/template.js b/templates/dualsense/template.js index 15aac6f..4f10a33 100644 --- a/templates/dualsense/template.js +++ b/templates/dualsense/template.js @@ -1,4 +1,4 @@ -window.gamepad.template = class DualSenseTemplate { +window.gamepad.templateClass = class DualSenseTemplate { /** * Instanciates a new DualSense controller template */ diff --git a/templates/telemetry/template.css b/templates/telemetry/template.css index f7018ca..d78fcaa 100644 --- a/templates/telemetry/template.css +++ b/templates/telemetry/template.css @@ -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; +} diff --git a/templates/telemetry/template.html b/templates/telemetry/template.html index 49f5b3a..1fca8c6 100644 --- a/templates/telemetry/template.html +++ b/templates/telemetry/template.html @@ -1,5 +1,4 @@ -
-
+
@@ -15,8 +14,14 @@
0
-
+
+
+
+

Telemetry Wizard

+
+
+
diff --git a/templates/telemetry/template.js b/templates/telemetry/template.js index 5b3e78a..6faddd0 100644 --- a/templates/telemetry/template.js +++ b/templates/telemetry/template.js @@ -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'); + } + } + + 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(resolve); + return; + } + this.drawChart(resolve); + }; + this.gamepad.$gamepad.appendChild(script); + }); } /** * Initializes the live chart */ - initChart() { - 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(); - return; - } - this.drawChart(); - }; - this.gamepad.$gamepad.appendChild(script); + 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 = ` +

Inputs

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

Chart widget

+
+
+ + +
+
+ + s +
+
+

Meters widget

+
+
+ + +
+
+

Steering widget

+
+
+ + ° +
+
+

Display mode

+
+ + +
+
+

Then, press any button to continue.

+ `; + + 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 = ` +

Waiting for ${name} activity.

+ `; + const { type, index } = await this.detectActivity(); + if (type === 'button') { + this.$wizardInstructions.innerHTML = ` +

Release the ${name} button.

+ `; + await this.waitButtonRelease(index); + return { type, index, releasedValue: 0, pressedValue: 1 }; + } + + this.$wizardInstructions.innerHTML = ` +

Press and hold the ${name} axis.

+ `; + const pressedValue = await this.getAxisPush(index); + + this.$wizardInstructions.innerHTML = ` +

Release the ${name} axis.

+ `; + const releasedValue = await this.getAxisPush(index, pressedValue); + + return { type, index, releasedValue, pressedValue }; + } + + /** + * Calibrates the steering axis + * + * @returns {Promise} + */ + async calibrateSteering() { + this.$wizardInstructions.innerHTML = ` +

Turn the steering axis all the way to the left.

+ `; + const { index } = await this.detectActivity('axis', 0.2); + const leftValue = await this.getAxisPush(index, 0); + + this.$wizardInstructions.innerHTML = ` +

Turn the steering axis all the way to the right.

+ `; + 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('&'); + } }; diff --git a/templates/xbox-one/template.css b/templates/xbox-one/template.css index 83a1607..fddebce 100644 --- a/templates/xbox-one/template.css +++ b/templates/xbox-one/template.css @@ -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; diff --git a/templates/xbox-one/template.html b/templates/xbox-one/template.html index b769980..0231256 100644 --- a/templates/xbox-one/template.html +++ b/templates/xbox-one/template.html @@ -1,32 +1,34 @@ -
- - - -
-
- - - -
-
-
- - - -
-
- - - - -
-
- - -
-
- - - - +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + + +
+
+ + +
+
+ + + + +
diff --git a/templates/xbox-one/template.js b/templates/xbox-one/template.js index 9e63138..eb1089d 100644 --- a/templates/xbox-one/template.js +++ b/templates/xbox-one/template.js @@ -1,4 +1,4 @@ -window.gamepad.template = class XboxOneTemplate { +window.gamepad.templateClass = class XboxOneTemplate { /** * Instanciates a new Xbox One controller template */