// 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 = `${ticks}${redZone}${unit}${max === 7000 ? 'TR/MIN' : unit}—${label}${level}${mini}`; 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 }; }