// Instrument rendering and telemetry projection; no network or persistence.
export function initInstruments() {
const $ = selector => document.querySelector(selector);
const icons = {
"fuelPercent": "",
"coolantCelsius": ""
};
function makeGauge(id, max, step, unit, label) {
const point = (angle, radius) => {
const r = angle * Math.PI / 180;
return [150 + radius * Math.sin(r), 150 - radius * Math.cos(r)];
};
let ticks = '';
for (let v = 0; v <= max; v += step / 4) {
const angle = -130 + v / max * 260;
const major = v % step === 0;
const a = point(angle, major ? 112 : 119), b = point(angle, 126), n = point(angle, 96);
ticks += ``;
if (major) ticks += `${max === 7000 ? v / 1000 : v}`;
}
// Decorative demonstration red zone; not a calibrated vehicle redline.
const redStart = point(-130 + 5500 / 7000 * 260, 128), redEnd = point(130, 128);
const redZone = max === 7000 ? `` : '';
const levelKey = max === 7000 ? 'coolantCelsius' : 'fuelPercent';
const iconPath = icons[levelKey];
// Compact auxiliary arc, separated from the main instrument.
const levelPoint = (angle, radius = 64) => [150+radius*Math.sin(angle*Math.PI/180),244-radius*Math.cos(angle*Math.PI/180)];
// 86.8 degrees: 30% shorter than the previous 124-degree auxiliary arc.
const arcPath = (from,to) => `M${levelPoint(from).join(' ')} A64 64 0 0 0 ${levelPoint(to).join(' ')}`;
const arc = arcPath(223.4,136.6);
const redArc = max===7000 ? arcPath(153.96,136.6) : arcPath(223.4,212.984);
const tickRatios = max === 7000 ? [0,.28,.56,.84,1] : [0,.12,.4133333333,.7066666667,1];
const levelTicks = tickRatios.map((fraction,index) => {
const angle = 223.4 - 86.8*fraction;
const inner=levelPoint(angle,54), outer=levelPoint(angle,61);
return ``;
}).join('');
const level = `${levelTicks}${max===7000?'C':'E'}${max===7000?'H':'F'}${max===7000?'':'½'}${iconPath}— ${max===7000?'°C':'%'}`;
const low = max === 7000 ? 'C' : 'E', high = max === 7000 ? 'H' : 'F';
const mini = `${low}${high}${max===7000?'':`½`}${iconPath}— ${max===7000?'°C':'%'}`;
$(id).innerHTML = ``;
return value => {
const valid = typeof value === 'number' && Number.isFinite(value);
$(`${id} .dial-value`).textContent = valid ? Math.round(value).toLocaleString('fr-FR') : '—';
$(`${id} .digital-progress`).style.strokeDashoffset = String(valid ? 100 - Math.max(0,Math.min(1,value/max))*100 : 100);
$(`${id} .needle`).style.opacity = valid ? '1' : '0';
if (valid) $(`${id} .needle`).style.transform = `rotate(${-130 + Math.max(0, Math.min(max, value)) / max * 260}deg)`;
$(`${id} svg`).setAttribute('aria-label', `${label} : ${valid ? Math.round(value) : 'indisponible'} ${max === 7000 ? 'tr/min' : 'km/h'}`);
};
}
const speedGauge = makeGauge('#speed-gauge', 220, 20, 'KM/H', 'VITESSE');
const rpmGauge = makeGauge('#rpm-gauge', 7000, 1000, '× 1 000 TR/MIN', 'RÉGIME MOTEUR');
function updateLevels(data) {
for (const key of ['fuelPercent', 'coolantCelsius']) {
const value = data?.[key], valid = Number.isFinite(value);
const min = key === 'fuelPercent' ? 0 : 40, max = key === 'fuelPercent' ? 100 : 130;
const ratio = valid ? Math.max(0,Math.min(1,(value-min)/(max-min))) : 0;
const analog = document.querySelector(`[data-analog-level="${key}"]`);
analog.querySelector('.mini-needle').style.transform = `rotate(${-90 + ratio*180}deg)`;
analog.querySelector('.mini-needle').style.opacity = valid ? '1' : '0';
analog.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
const digital = document.querySelector(`[data-digital-level="${key}"]`);
const stops = key === 'fuelPercent' ? [0,.12,.4133333333,.7066666667,1] : [0,.28,.56,.84,1];
const stepped = stops.filter(stop => stop <= ratio).at(-1) ?? 0;
const progress = digital.querySelector('.level-arc-progress');
progress.style.strokeDashoffset = String(100*(1-stepped));
progress.style.opacity = valid && stepped > 0 ? '1' : '0';
digital.classList.toggle('last-level', valid && key === 'coolantCelsius' && ratio >= .84);
digital.classList.toggle('reserve-level', valid && key === 'fuelPercent' && stepped === .12);
digital.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
for (const el of [analog, digital]) {
el.setAttribute('role', 'meter');
el.setAttribute('aria-label', key === 'fuelPercent' ? 'Carburant' : 'Température moteur');
el.setAttribute('aria-valuemin', String(min)); el.setAttribute('aria-valuemax', String(max));
if (valid) el.setAttribute('aria-valuenow', String(value)); else el.removeAttribute('aria-valuenow');
el.setAttribute('aria-valuetext', valid ? value + (key === 'fuelPercent' ? ' %' : ' °C') : 'Indisponible');
}
}
}
return { speedGauge, rpmGauge, updateLevels };
}