Files
CARIA-AUTOMOTIVE/ui/instruments.js
T

83 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Instrument rendering and telemetry projection; no network or persistence.
export function initInstruments() {
const $ = selector => document.querySelector(selector);
const icons = {
"fuelPercent": "<path d=\"M4 21V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v16M3 21h12M4 10h10m3-6 3 3v11a2 2 0 0 1-4 0v-5a2 2 0 0 0-2-2m4-6v4h2\"/>",
"coolantCelsius": "<path d=\"M10 12V4a2 2 0 0 1 4 0v8a3.5 3.5 0 1 1-4 0Zm2-6v9m4-9h2m-2 3h2M2 19q2-2 4 0t4 0t4 0t4 0t4 0M2 23q2-2 4 0t4 0t4 0t4 0t4 0\"/>"
};
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 += `<line class="tick ${major ? 'major' : v % (step / 2) === 0 ? 'intermediate' : 'quarter'}" x1="${a[0]}" y1="${a[1]}" x2="${b[0]}" y2="${b[1]}"/>`;
if (major) ticks += `<text class="dial-number" x="${n[0]}" y="${n[1] + 4}">${max === 7000 ? v / 1000 : v}</text>`;
}
// 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 ? `<defs><linearGradient id="rpm-red-zone" gradientUnits="userSpaceOnUse" x1="${redStart[0]}" y1="${redStart[1]}" x2="${redEnd[0]}" y2="${redEnd[1]}"><stop stop-color="#f7a65d" stop-opacity="0.1"/><stop offset="0.5" stop-color="#f67855"/><stop offset="1" stop-color="#fa4d5b"/></linearGradient></defs><path class="rpm-red-zone" d="M${redStart.join(' ')} A128 128 0 0 1 ${redEnd.join(' ')}"/>` : '';
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 `<path class="level-tick" d="M${inner.join(' ')} L${outer.join(' ')}"/>`;
}).join('');
const level = `<g class="digital-levels" data-digital-level="${levelKey}"><path class="level-arc-track" d="${arc}"/><path class="level-arc-progress" pathLength="100" d="${arc}"/><path class="level-arc-red" d="${redArc}"/>${levelTicks}<text class="level-end" x="95" y="304">${max===7000?'C':'E'}</text><text class="level-end" x="205" y="304">${max===7000?'H':'F'}</text>${max===7000?'':'<text class="level-end" x="150" y="325">½</text>'}<g class="level-icon" transform="translate(${max===7000?139.8:141.6} ${max===7000?267.8:269.6}) scale(${max===7000?.85:.7})" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${iconPath}</g><text class="level-value" x="150" y="348"><tspan data-value="${levelKey}">—</tspan> ${max===7000?'°C':'%'}</text></g>`;
const low = max === 7000 ? 'C' : 'E', high = max === 7000 ? 'H' : 'F';
const mini = `<path class="analog-separator" d="M59.590 240.609 L77 226 L223 226 L240.410 240.609"/><g class="analog-levels" data-analog-level="${levelKey}"><g transform="translate(150 264) scale(0.90) translate(-150 -264)"><g transform="translate(0 572) scale(1 -1)"><path class="mini-track" d="M90 308 A60 60 0 0 1 210 308"/><path class="mini-red" d="${max===7000?'M192.4 265.6 A60 60 0 0 1 210 308':'M90 308 A60 60 0 0 1 107.6 265.6'}"/><path class="mini-ticks" d="M90 308h8M107.6 265.6l6 6M150 248v8M192.4 265.6l-6 6M210 308h-8"/><text class="mini-end" transform="translate(0 604) scale(1 -1)" x="104" y="302">${low}</text><text class="mini-end" transform="translate(0 604) scale(1 -1)" x="196" y="302">${high}</text>${max===7000?'':`<text class="mini-end" transform="translate(0 520) scale(1 -1)" x="150" y="260">½</text>`}<line class="mini-needle" x1="150" y1="308" x2="150" y2="258"/><circle class="mini-hub" cx="150" cy="308" r="3"/></g></g><g class="level-icon" transform="translate(${max===7000?139.8:141.6} ${max===7000?277.8:279.6}) scale(${max===7000?.85:.7})" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${iconPath}</g><text class="level-value" x="150" y="368"><tspan data-value="${levelKey}">—</tspan> ${max===7000?'°C':'%'}</text></g>`;
$(id).innerHTML = `<svg viewBox="0 0 300 265" role="img" aria-label="${label} : en attente"><path class="dial-rim" d="M68.4 255 A133 133 0 1 1 231.6 255 Z" fill="none" stroke="var(--border)"/><path class="dial-light" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/><path class="digital-track" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/><path class="digital-progress" pathLength="100" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/>${ticks}${redZone}<text x="150" y="110" class="dial-label analog-unit">${unit}</text><text x="150" y="110" class="dial-label digital-unit">${max === 7000 ? 'TR/MIN' : unit}</text><line class="needle" x1="150" y1="150" x2="150" y2="43" style="transform-origin:150px 150px;transform:rotate(-130deg)"/><circle class="needle-hub" cx="150" cy="150" r="7" fill="var(--accent)"/><circle class="needle-hub" cx="150" cy="150" r="3" fill="var(--surface)"/><text x="150" y="212" class="dial-value">—</text><text x="150" y="239" class="dial-label">${label}</text>${level}${mini}</svg>`;
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 };
}