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
I couldn't find the piece of music playing when the helicopters are taking off from camp omega, neither on soundtracks, be they regular or extended, nor in the game audio files. You can hear it here at 7m40s:
<center>
<iframe width="560" height="315" src="https://www.youtube.com/embed/G80bgC6koq8?start=460" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</center>
My last hope was to rip the audio from the cutscene itself, hoping effects and music were mixed dynamically.
They weren't.
Just making the code public for posterity. Using php (command line) 'cause I'm a weirdo.
<?php
/*******************************************************************************
Rips the audio from a Metal Gear Solid Ground Zeroes cutscene (FSM file)
http://metalgearmodding.wikia.com/wiki/FSM
ground-zeroes-fsm-sound-extractor
│ ground-zeroes-fsm-sound-extractor.phpcli
│
├───files
└───tools
├───revorb
│ revorb.exe
│
└───ww2ogg
packed_codebooks_aoTuV_603.bin
ww2ogg.exe
*******************************************************************************/
echo "Start\n";
$fileIn = './files/p11_010100_000.fsm';
$fileOut = $fileIn.'.wem';
$bufSize = 256*1024;
$chunksPos = array();
$fh = fopen($fileIn, "rb");
//********************************************** build sound chunks offsets list
echo "Building chunks list\n";
while($fh && !feof($fh)) {
$bufPos = ftell($fh);
$buf = fread($fh, $bufSize);
$off = 0;
while(true) {
$pos = strpos($buf, 'SND ', $off);
if($pos === false) {
break;
}
$off = $pos+1;
$chunksPos[] = $bufPos + $pos;
}
}
echo "Found chunks: ".count($chunksPos)."\n";
//*************************************************** recompose wwise audio file
echo "Recomposing sound file\n";
$file = 0;
file_put_contents($fileOut, '');
foreach($chunksPos as $pos) {
++$file;
// first chunk has a larger header
$headerSize = ($file === 1) ? 32 : 16;
fseek($fh, $pos);
$buf = fread($fh, $headerSize);
$data = unpack(
'A4sig/Vclen/Vs1/Vs2/H*',
$buf
);
$chunk = stream_get_line($fh, $data['clen'] - $headerSize);
file_put_contents(
$fileOut,
$chunk,
FILE_APPEND
);
}
fclose($fh);
//********************************************************* convert wwise to ogg
echo "Converting sound file\n";
`"./tools/ww2ogg/ww2ogg.exe" "$fileOut" -o "$fileOut.ww2ogg.ogg" --pcb "./tools/ww2ogg/packed_codebooks_aoTuV_603.bin"`;
//***************************************************************** optimize ogg
echo "Optimizing sound file\n";
`"./tools/revorb/revorb.exe" "$fileOut.ww2ogg.ogg" "$fileOut.revorb.ogg"`;
echo "done\n";
It would probably have been cheaper to buy a new one, but much less fun.
There isn't much to explain here, it's cutting foam and sewing fabric. You just stab the fabric with some thread and put the foam inside, quite easy. There is also some stapling, sure.
Just marvel at the pictures, unlike me, who can only see the flaws.
Part 1, the armrests
--------------------
<center>
[![](http://spenibus.net/f/t/cY)](http://spenibus.net/f/g/cY)
[![](http://spenibus.net/f/t/cZ)](http://spenibus.net/f/g/cZ)
[![](http://spenibus.net/f/t/d0)](http://spenibus.net/f/g/d0)
[![](http://spenibus.net/f/t/d1)](http://spenibus.net/f/g/d1)
[![](http://spenibus.net/f/t/d2)](http://spenibus.net/f/g/d2)
[![](http://spenibus.net/f/t/d3)](http://spenibus.net/f/g/d3)
[![](http://spenibus.net/f/t/cX)](http://spenibus.net/f/g/cX)
</center>
Part 2, the headrest
--------------------
<center>
[![](http://spenibus.net/f/t/d4)](http://spenibus.net/f/g/d4)
[![](http://spenibus.net/f/t/d5)](http://spenibus.net/f/g/d5)
[![](http://spenibus.net/f/t/d6)](http://spenibus.net/f/g/d6)
[![](http://spenibus.net/f/t/d7)](http://spenibus.net/f/g/d7)
[![](http://spenibus.net/f/t/d9)](http://spenibus.net/f/g/d9)
</center>
Part 3, the backrest
--------------------
<center>
[![](http://spenibus.net/f/t/da)](http://spenibus.net/f/g/da)
[![](http://spenibus.net/f/t/db)](http://spenibus.net/f/g/db)
[![](http://spenibus.net/f/t/dc)](http://spenibus.net/f/g/dc)
[![](http://spenibus.net/f/t/dd)](http://spenibus.net/f/g/dd)
[![](http://spenibus.net/f/t/de)](http://spenibus.net/f/g/de)
[![](http://spenibus.net/f/t/df)](http://spenibus.net/f/g/df)
[![](http://spenibus.net/f/t/dh)](http://spenibus.net/f/g/dh)
[![](http://spenibus.net/f/t/dg)](http://spenibus.net/f/g/dg)
</center>
Part 4, the seat
----------------
<center>
[![](http://spenibus.net/f/t/dp)](http://spenibus.net/f/g/dp)
[![](http://spenibus.net/f/t/di)](http://spenibus.net/f/g/di)
[![](http://spenibus.net/f/t/dj)](http://spenibus.net/f/g/dj)
[![](http://spenibus.net/f/t/dk)](http://spenibus.net/f/g/dk)
[![](http://spenibus.net/f/t/dl)](http://spenibus.net/f/g/dl)
[![](http://spenibus.net/f/t/dm)](http://spenibus.net/f/g/dm)
[![](http://spenibus.net/f/t/do)](http://spenibus.net/f/g/do)
[![](http://spenibus.net/f/t/dn)](http://spenibus.net/f/g/dn)
</center>
All done
--------
[![](http://spenibus.net/f/t/dq)](http://spenibus.net/f/g/dq)
The end.