You can check the [original post][1].
Just a little code update, now with the gearbox, pure CSS rendering, adaptive resolution and pixel perfect alignment.
Demo
----
<center><iframe width="560" height="315" src="https://www.youtube.com/embed/vJhXbPwUJ-w" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></center>
wheel.html
----------
````
<!DOCTYPE html>
<html>
<head>
<title>Wheel</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script src="wheel.js" type="text/javascript"></script>
<link href="wheel2.css" rel="stylesheet" type="text/css" />
<link href="wheelScalable.css" rel="stylesheet" type="text/css" />
</head>
<body class="index">
<div id="viewer">
<span id="unitRef"></span>
<span class="viewerCol">
<span id="wheel">
<span id="wheelA">
<span id="wheelB">
<span id="wheelC">
<span class="partA"></span>
<span class="partB"></span>
<span class="partC"></span>
</span>
</span>
<span id="wheelTopNotch"></span>
</span>
</span>
</span>
<span class="viewerCol">
<span id="gears">
<span id="gearRowBack"><span></span></span>
<span id="gearCol1" class="gearCol forward"><span></span></span>
<span id="gearCol2" class="gearCol forward"><span></span></span>
<span id="gearCol3" class="gearCol forward"><span></span></span>
<span id="gearCol4" class="gearCol reverse"><span></span></span>
<span id="gearColNegative1" class="gearColNegative gearColNegativeTop "><span></span></span>
<span id="gearColNegative2" class="gearColNegative gearColNegativeTop "><span></span></span>
<span id="gearColNegative3" class="gearColNegative gearColNegativeTop "><span></span></span>
<span id="gearColNegative4" class="gearColNegative gearColNegativeBottom"><span></span></span>
<span id="gearColNegative5" class="gearColNegative gearColNegativeBottom"><span></span></span>
<span id="gearColNegative6" class="gearColNegative gearColNegativeBottom"><span></span></span>
<span id="gearRowFront"><span></span></span>
<span id="gearStick"><span class="gearN"></span></span>
</span>
<span id="pedals">
<span id="clutch" class="pedal"><span><span></span></span></span>
<span id="brake" class="pedal"><span><span></span></span></span>
<span id="throttle" class="pedal"><span><span></span></span></span>
</span>
</span>
</div>
<div>
<legend><input type="checkbox" id="showDisplay"> Show device data</legend>
</div>
<div id="dataDisplay">waiting for input</div>
</body>
</html>
````
wheelScalable.css
---------
````
body {
background-color:transparent;
margin:0;
padding:0;
}
#unitRef {
display:block;
width:1vw;
height:1vw;
background-color:green;
position:fixed;
left:-2vw;
}
#viewer {
display:inline-block;
height:50vw;
background-color:rgba(0,0,0,0.2);
font-size:0;
white-space:nowrap;
border-bottom:1vw red solid;
}
.viewerCol {
display:inline-block;
vertical-align:bottom;
height:100%;
}
/******************************************************************************/
#wheel_old {
display:inline-block;
background-color:rgba(0,255,0,0.2);
height:46vw;
margin:2vw;
display:none;
}
#wheel {
display:inline-block;
position:relative;
}
#wheelTopNotch {
width:2vw;
height:4vw;
background-color:#468;
position:absolute;
left:18vw;
top: -1vw;
}
#wheelA {
display:inline-block;
width:38vw;
height:38vw;
margin:4vw;
border:1vw solid #468;
border:1vw solid black;
border-radius:22vw;
overflow:hidden;
position:relative;
}
#wheelB {
margin:-1vw;
display:inline-block;
width:34vw;
height:34vw;
border:3vw solid #ACF;
border:3vw solid #AAA;
border-radius:22vw;
overflow:hidden;
position:relative;
}
#wheelC {
margin:-1vw;
display:inline-block;
width:32vw;
height:32vw;
border:2vw solid #468;
border:2vw solid black;
border-radius:22vw;
overflow:hidden;
position:relative;
}
#wheel .partA {
display:inline-block;
width:60vw;
height:60vw;
border:5vw solid #468;
border-radius:35vw;
position:absolute;
left:-19vw;
top: 15vw;
}
#wheel .partB {
display:inline-block;
background-color:#468;
width:8vw;
height:20vw;
position:absolute;
left:12vw;
top: 16vw;
}
/******************************************************************************/
#gears {
display:inline-block;
margin:2vw 2vw 0vw 0vw;
position:relative;
}
.gearCol {
display:inline-block;
vertical-align:bottom;
width:4vw;
height:18vw;
margin:1vw;
background-color:black;
border-radius:2vw;
position:relative;
}
.gearCol > span {
display:inline-block;
vertical-align:top;
background-color:#AAA;
width:2vw;
height:16vw;
margin:1vw;
border-radius:1vw;
}
#gearRowBack {
display:block;
background-color:black;
width:22vw;
height:4vw;
border-radius:2vw;
position:absolute;
left:1vw;
top:8vw;
}
#gearRowFront {
display:block;
background-color:#AAA;
width:20vw;
height:2vw;
border-radius:2vw;
position:absolute;
left:2vw;
top:9vw;
}
#gearCol4 {
height:11vw;
}
#gearCol4 > span {
height:9vw;
}
.gearColNegative {
position:absolute;
display:inline-block;
border:1vw solid #AAA;
}
.gearColNegative > span {
width:2vw;
height:4vw;
display:inline-block;
border:1vw solid black;
}
.gearColNegativeTop
,.gearColNegativeTop > span {
border-radius: 0vw 0vw 20vw 20vw;
border-top:none;
}
.gearColNegativeBottom
,.gearColNegativeBottom > span {
border-radius: 20vw 20vw 0vw 0vw;
border-bottom:none;
}
#gearColNegative1 {
top:4vw;
left:3vw;
}
#gearColNegative2 {
top:4vw;
left:9vw;
}
#gearColNegative3 {
top:4vw;
left:15vw;
border-right:none;
border-radius: 0vw 0vw 0vw 20vw;
}
#gearColNegative3 > span {
border-right:none;
border-radius: 0vw 0vw 0vw 20vw;
width:1vw;
}
#gearColNegative4 {
top:10vw;
left:3vw;
}
#gearColNegative5 {
top:10vw;
left:9vw;
}
#gearColNegative6 {
top:10vw;
left:15vw;
}
#gearStick {
display:block;
background-color:#246;
width: 6vw;
height:6vw;
border-radius:3vw;
position:absolute;
top: 7vw;
left:6vw;
}
#gearStick > span {
display:block;
background-color:#ACF;
width:4vw;
height:4vw;
border-radius:2vw;
margin:1vw;
}
#gearStick.gearF1 {
top: 0vw;
left:0vw;
}
#gearStick.gearF2 {
top: 14vw;
left:0vw;
}
#gearStick.gearF3 {
top: 0vw;
left:6vw;
}
#gearStick.gearF4 {
top: 14vw;
left:6vw;
}
#gearStick.gearF5 {
top: 0vw;
left:12vw;
}
#gearStick.gearF6 {
top: 14vw;
left:12vw;
}
#gearStick.gearR1 {
top: 14vw;
left:18vw;
}
/******************************************************************************/
#pedals {
display:block;
margin:2vw 2vw 0vw 0vw;
}
.pedal {
display:inline-block;
vertical-align:bottom;
background-color:#AAA;
width:4vw;
height:22vw;
margin:0 1vw;
border:1vw solid black;
}
.pedal > span {
display:inline-block;
vertical-align:top;
background-color:#AAA;
width:100%;
height:50%;
}
#throttle {
background-color:#0A0;
}
#brake {
background-color:#C00;
}
#clutch {
background-color:#FA0;
}
````
wheel.js
--------
````
/*******************************************************************************
pc steering wheel animated viewer
probably firefox only
2019-08-10 19:55 +0000
/******************************************************************************/
console.log('wheel');
let interval;
/*
// chrome compatibility
if(!('ongamepadconnected' in window)) {
console.log('no event');
interval = setInterval( z=>{
console.log('polling');
window.dispatchEvent(new CustomEvent('gamepadconnected'))
}, 1000);
}
*/
let device;
let dataDisplay;
let showDisplay;
// mapping the axes
let axisIndexWheel = 0;
let axisIndexThrottle = 1;
let axisIndexBrake = 2;
let axisIndexClutch = 3;
let axisIndexClutchAlt = 5;
// mapping the buttons
let btnIndexR1 = 11;
let btnIndexF1 = 12;
let btnIndexF2 = 13;
let btnIndexF3 = 14;
let btnIndexF4 = 15;
let btnIndexF5 = 16;
let btnIndexF6 = 17;
// elements references
let elemWheel;
let elemThrottle;
let elemBrake;
let elemClutch;
let elemGearStick;
// misc
let axisWheelFullRotation = 900; // degrees
let loopStartTime = (new Date()).getTime();
let loopEndTime = (new Date()).getTime();
//let renderRate = 1000/100;
//******************************************************************************
window.addEventListener('DOMContentLoaded', function(e) {
console.log('loaded');
// style: convert vw into px for scalability with pixel perfect alignment
let xhr = new XMLHttpRequest();
xhr.open('GET', './wheelScalable.css', true);
xhr.responseType = 'text';
xhr.onload = function(data){
let val = data.target.responseText;
// get unit ref for pixel perfect consistency
let unit = Math.floor(
window.getComputedStyle(
document.getElementById('unitRef')
)
.getPropertyValue('width')
.match(/([\d\.]+)/)[1]
);
let r = /([^\d])([\d\.]+)(\s*vw)/g;
let m;
while(m = r.exec(val)) {
val = val.replace(
new RegExp(m[0], 'g')
,m[1]+ unit * parseInt(m[2]) +'px'
);
}
let s = document.createElement('style');
s.textContent = val;
document.head.appendChild(s);
}
xhr.send();
dataDisplay = document.getElementById('dataDisplay');
showDisplay = document.getElementById('showDisplay');
elemWheel = document.getElementById('wheel');
elemThrottle = document.querySelector('#throttle > span');
elemBrake = document.querySelector('#brake > span');
elemClutch = document.querySelector('#clutch > span');
elemGearStick = document.querySelector('#gearStick');
/*
window.addEventListener('gamepadconnected', function(e) {
if(e.gamepad.id.match(/g920.*wheel/i)) {
wheelDevice = e.gamepad;
gameLoop();
return;
}
});
*/
gameLoop();
});
//******************************************************************************
function gameLoop() {
/* framerate limiter *
loopStartTime = (new Date()).getTime();
let delta = loopStartTime-loopEndTime;
if(delta < renderRate) {
requestAnimationFrame(gameLoop);
return;
}
console.log('render delta: '+delta+'ms')
*/
wheelDevice = null;
let gps = navigator.getGamepads();
for(let gp of gps) {
if(gp.id.match(/g920.*wheel/i)) {
wheelDevice = gp;
break;
}
}
if(wheelDevice) {
//******************************************************* data gathering
let axisValueWheel = wheelDevice.axes[axisIndexWheel];
let axisValueThrottle = wheelDevice.axes[axisIndexThrottle];
let axisValueBrake = wheelDevice.axes[axisIndexBrake];
//let axisValueClutch = wheelDevice.axes[axisIndexClutch];
let axisValueClutch = wheelDevice.axes[axisIndexClutch] || wheelDevice.axes[axisIndexClutchAlt];
// convert wheel axis to angle
axisValueWheel = Math.round(axisWheelFullRotation / 2 * axisValueWheel * 1000) / 1000;
// normalize pedals axes to range 0-1
axisValueThrottle = (axisValueThrottle * -1 + 1) / 2;
axisValueBrake = (axisValueBrake * -1 + 1) / 2;
axisValueClutch = (axisValueClutch * -1 + 1) / 2;
//********************************************************* data display
let debugText = '-';
if(showDisplay.checked) {
debugText = '\n[id]\n '+wheelDevice.id+'\n';
debugText += '\n[Axes]';
wheelDevice.axes.forEach((a,i)=>{
debugText += "\n "+i+' : '+a;
});
debugText += '\n\n[Buttons]';
wheelDevice.buttons.forEach((b,i)=>{
debugText += "\n "+i+' : pressed:'+b.pressed+' touched:'+b.touched;
});
debugText = '<pre>\n[Computed]\n'
+' wheel: '+axisValueWheel +'deg\n'
+' thottle: '+axisValueThrottle+'\n'
+' brake: '+axisValueBrake +'\n'
+' clutch: '+axisValueClutch +'\n'
+'</pre>'
+'<pre>'+debugText+'</pre>'
;
}
dataDisplay.innerHTML = debugText;
//************************************************************** visuals
axisValueWheel = 'rotate('+axisValueWheel+'deg)';
if(elemWheel.style.transform != axisValueWheel) {
elemWheel.style.transform = axisValueWheel;
}
axisValueThrottle = (100 - axisValueThrottle * 100)+'%';
if(elemThrottle.style.height != axisValueThrottle) {
elemThrottle.style.height = axisValueThrottle;
}
axisValueBrake = (100 - axisValueBrake * 100)+'%';
if(elemBrake.style.height != axisValueBrake) {
elemBrake.style.height = axisValueBrake;
}
axisValueClutch = (100 - axisValueClutch * 100)+'%';
if(elemClutch.style.height != axisValueClutch) {
elemClutch.style.height = axisValueClutch;
}
let gearStickClass = ''
+(wheelDevice.buttons[btnIndexR1].pressed ? ' gearR1' : '')
+(wheelDevice.buttons[btnIndexF1].pressed ? ' gearF1' : '')
+(wheelDevice.buttons[btnIndexF2].pressed ? ' gearF2' : '')
+(wheelDevice.buttons[btnIndexF3].pressed ? ' gearF3' : '')
+(wheelDevice.buttons[btnIndexF4].pressed ? ' gearF4' : '')
+(wheelDevice.buttons[btnIndexF5].pressed ? ' gearF5' : '')
+(wheelDevice.buttons[btnIndexF6].pressed ? ' gearF6' : '')
;
if(elemGearStick.classList != gearStickClass) {
elemGearStick.classList = gearStickClass
}
}
//loopEndTime = (new Date()).getTime();
requestAnimationFrame(gameLoop);
/*
let angle = Math.round(wheelFullRotation / 2 * axis0value * 1000) / 1000;
nodeAxis0.textContent = axis0value+' : '+angle+'deg';
wheelGfx.style.transform = 'rotate('+angle+'deg)';
*/
//transform: rotate(20deg);
/*
var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
if (!gamepads) {
return;
}
var gp = gamepads[0];
if (buttonPressed(gp.buttons[0])) {
b--;
} else if (buttonPressed(gp.buttons[2])) {
b++;
}
if (buttonPressed(gp.buttons[1])) {
a++;
} else if (buttonPressed(gp.buttons[3])) {
a--;
}
ball.style.left = a * 2 + "px";
ball.style.top = b * 2 + "px";
*/
}
````
[1]: http://spenibus.net/b/p/F/PC-steering-wheel-viewer-prototype-in-html-javascript