feat(hud): Dreier-Konsole mit Finimeter und Tiefenmesser
Statt zweier Zahlenfelder jetzt zwei Zeigerinstrumente links und rechts vom Luft-Ring - wie eine echte Tauchkonsole. Der Ring bleibt in der Mitte, weil die Restzeit die Zahl ist, auf die es ankommt. - Finimeter 0-300 bar, roter Bereich = Reserve unter reservePressure - Tiefenmesser 0-depthDialMax, roter Bereich = Tiefengrenze des Atemgases. Der faengt bei Pressluft bei 45 m an und bei Trimix erst bei 100 m - der Gefahrenbereich haengt also an der getragenen Flasche. Gibt es real nicht, hilft im Spiel aber sofort beim Einschaetzen - Skalenzahlen bewusst weggelassen: bei 96 px kollidieren sie mit allem und liest ohnehin niemand. Zeiger zeigt die Tendenz, die Zahl in der Uhr den Wert - Zeiger und Wert faerben sich rot, sobald der rote Bereich erreicht ist Beim Bauen gefunden: CSS transform-origin auf dem Zeiger wird zusaetzlich zum rotate(winkel 50 50) im Attribut angewendet, der Drehpunkt wandert dadurch doppelt und der Zeiger landet ausserhalb der Uhr. transform-origin ist raus. README-Renderings auf die Konsole umgebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+31
-6
@@ -6,11 +6,24 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="hud" class="hidden">
|
||||
<div class="readout readout--right">
|
||||
<span id="depth-label" class="caption">TIEFE</span>
|
||||
<div class="value"><span id="depth-value">0.0</span><span class="unit">m</span></div>
|
||||
<!-- Finimeter: Restdruck der Flasche, roter Bereich = Reserve -->
|
||||
<div class="dial" id="pressure">
|
||||
<div class="dial-face">
|
||||
<svg viewBox="0 0 100 100" aria-hidden="true">
|
||||
<circle class="face" cx="50" cy="50" r="46"></circle>
|
||||
<path class="zone" id="pressure-zone"></path>
|
||||
<g class="ticks" id="pressure-ticks"></g>
|
||||
<g class="needle" id="pressure-needle">
|
||||
<line x1="50" y1="58" x2="50" y2="17"></line>
|
||||
<circle cx="50" cy="50" r="3.4"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="dial-value"><span id="pressure-value">0</span><span class="unit">bar</span></div>
|
||||
</div>
|
||||
<span id="pressure-label" class="caption">DRUCK</span>
|
||||
</div>
|
||||
|
||||
<!-- Restluft als Ring, die Zahl auf die es ankommt -->
|
||||
<div class="gauge">
|
||||
<svg viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle class="gauge-track" cx="60" cy="60" r="52"></circle>
|
||||
@@ -22,9 +35,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="readout" id="pressure">
|
||||
<span id="pressure-label" class="caption">DRUCK</span>
|
||||
<div class="value"><span id="pressure-value">0</span><span class="unit">bar</span></div>
|
||||
<!-- Tiefenmesser: roter Bereich = Tiefengrenze des Atemgases -->
|
||||
<div class="dial" id="depth">
|
||||
<div class="dial-face">
|
||||
<svg viewBox="0 0 100 100" aria-hidden="true">
|
||||
<circle class="face" cx="50" cy="50" r="46"></circle>
|
||||
<path class="zone" id="depth-zone"></path>
|
||||
<g class="ticks" id="depth-ticks"></g>
|
||||
<g class="needle" id="depth-needle">
|
||||
<line x1="50" y1="58" x2="50" y2="17"></line>
|
||||
<circle cx="50" cy="50" r="3.4"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="dial-value"><span id="depth-value">0.0</span><span class="unit">m</span></div>
|
||||
</div>
|
||||
<span id="depth-label" class="caption">TIEFE</span>
|
||||
</div>
|
||||
|
||||
<div id="lamp" class="lamp">
|
||||
|
||||
+101
-10
@@ -1,17 +1,36 @@
|
||||
// Das HUD ist reine Anzeige - es sendet nichts zurueck an den Client.
|
||||
// Der Lua-Client schickt den kompletten Zustand, damit die Locale-Texte an einer
|
||||
// Stelle bleiben und hier nichts uebersetzt werden muss.
|
||||
// Der Lua-Client schickt den kompletten Zustand samt Skalenendwerten, damit die
|
||||
// Locale-Texte und die Config an einer Stelle bleiben.
|
||||
|
||||
const RING = 327; // 2 * PI * 52, muss zu --ring in style.css passen
|
||||
|
||||
// Zeigerinstrument: 270-Grad-Skala, unten offen - wie ein echtes Finimeter.
|
||||
const DIAL_START = -135;
|
||||
const DIAL_SWEEP = 270;
|
||||
const DIAL_R = 42;
|
||||
|
||||
const hud = document.getElementById('hud');
|
||||
const gaugeFill = document.getElementById('gauge-fill');
|
||||
const airValue = document.getElementById('air-value');
|
||||
const depthValue = document.getElementById('depth-value');
|
||||
const pressure = document.getElementById('pressure');
|
||||
const pressureValue = document.getElementById('pressure-value');
|
||||
const lamp = document.getElementById('lamp');
|
||||
|
||||
const dials = {
|
||||
pressure: {
|
||||
root: document.getElementById('pressure'),
|
||||
needle: document.getElementById('pressure-needle'),
|
||||
zone: document.getElementById('pressure-zone'),
|
||||
ticks: document.getElementById('pressure-ticks'),
|
||||
value: document.getElementById('pressure-value')
|
||||
},
|
||||
depth: {
|
||||
root: document.getElementById('depth'),
|
||||
needle: document.getElementById('depth-needle'),
|
||||
zone: document.getElementById('depth-zone'),
|
||||
ticks: document.getElementById('depth-ticks'),
|
||||
value: document.getElementById('depth-value')
|
||||
}
|
||||
};
|
||||
|
||||
const labels = {
|
||||
air: document.getElementById('air-label'),
|
||||
depth: document.getElementById('depth-label'),
|
||||
@@ -19,6 +38,67 @@ const labels = {
|
||||
lamp: document.getElementById('lamp-label')
|
||||
};
|
||||
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
const toRad = (deg) => ((deg - 90) * Math.PI) / 180;
|
||||
|
||||
function pointOn(radius, deg) {
|
||||
const r = toRad(deg);
|
||||
return [50 + radius * Math.cos(r), 50 + radius * Math.sin(r)];
|
||||
}
|
||||
|
||||
/** Winkel für einen Messwert auf der Skala. */
|
||||
function angleFor(value, max) {
|
||||
const ratio = Math.min(1, Math.max(0, value / (max || 1)));
|
||||
return DIAL_START + DIAL_SWEEP * ratio;
|
||||
}
|
||||
|
||||
/** Striche einmalig zeichnen - die ändern sich nie. */
|
||||
function buildTicks(group) {
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const deg = DIAL_START + (DIAL_SWEEP * i) / 10;
|
||||
const major = i % 2 === 0;
|
||||
const [x1, y1] = pointOn(DIAL_R - 6, deg);
|
||||
const [x2, y2] = pointOn(major ? DIAL_R - 13 : DIAL_R - 10, deg);
|
||||
const line = document.createElementNS(NS, 'line');
|
||||
|
||||
line.setAttribute('x1', x1.toFixed(2));
|
||||
line.setAttribute('y1', y1.toFixed(2));
|
||||
line.setAttribute('x2', x2.toFixed(2));
|
||||
line.setAttribute('y2', y2.toFixed(2));
|
||||
|
||||
if (major) line.setAttribute('class', 'major');
|
||||
|
||||
group.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
buildTicks(dials.pressure.ticks);
|
||||
buildTicks(dials.depth.ticks);
|
||||
|
||||
/** Roter Bereich. Beim Finimeter die Reserve, beim Tiefenmesser die Grenze des Atemgases. */
|
||||
function setZone(dial, from, to, max) {
|
||||
if (!(max > 0) || from === undefined || from >= max) {
|
||||
dial.zone.removeAttribute('d');
|
||||
return;
|
||||
}
|
||||
|
||||
const a = angleFor(from, max);
|
||||
const b = angleFor(Math.min(to, max), max);
|
||||
const [x1, y1] = pointOn(DIAL_R - 2, a);
|
||||
const [x2, y2] = pointOn(DIAL_R - 2, b);
|
||||
const large = b - a > 180 ? 1 : 0;
|
||||
|
||||
dial.zone.setAttribute(
|
||||
'd',
|
||||
`M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${DIAL_R - 2} ${DIAL_R - 2} 0 ${large} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`
|
||||
);
|
||||
}
|
||||
|
||||
function setNeedle(dial, value, max) {
|
||||
dial.needle.setAttribute('transform', `rotate(${angleFor(value, max).toFixed(2)} 50 50)`);
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
const total = Math.max(0, Math.floor(seconds));
|
||||
const mm = String(Math.floor(total / 60)).padStart(2, '0');
|
||||
@@ -36,17 +116,26 @@ function render(state) {
|
||||
|
||||
gaugeFill.style.strokeDashoffset = String(RING * (1 - ratio));
|
||||
airValue.textContent = formatTime(state.oxygen);
|
||||
depthValue.textContent = state.depth.toFixed(1);
|
||||
pressureValue.textContent = String(Math.round(state.pressure ?? 0));
|
||||
pressure.classList.toggle('reserve', !!state.reserve);
|
||||
|
||||
// Warnstufen kommen als Restsekunden vom Client, damit sie zu
|
||||
// config.warnAtSeconds passen statt hier nochmal definiert zu werden.
|
||||
hud.classList.toggle('warn', state.level === 'warn');
|
||||
hud.classList.toggle('danger', state.level === 'danger');
|
||||
hud.classList.toggle('deep', !!state.deep);
|
||||
hud.classList.toggle('overlimit', !!state.overLimit);
|
||||
|
||||
const maxPressure = state.maxPressure || 300;
|
||||
dials.pressure.value.textContent = String(Math.round(state.pressure ?? 0));
|
||||
setNeedle(dials.pressure, state.pressure ?? 0, maxPressure);
|
||||
setZone(dials.pressure, 0, state.reservePressure ?? 0, maxPressure);
|
||||
dials.pressure.root.classList.toggle('alert', !!state.reserve);
|
||||
|
||||
const depthScale = state.depthScale || 100;
|
||||
dials.depth.value.textContent = state.depth.toFixed(1);
|
||||
setNeedle(dials.depth, state.depth, depthScale);
|
||||
setZone(dials.depth, state.depthLimit, depthScale, depthScale);
|
||||
dials.depth.root.classList.toggle('alert', !!state.overLimit);
|
||||
dials.depth.root.classList.toggle('deep', !state.overLimit && !!state.deep);
|
||||
|
||||
lamp.classList.toggle('hidden', !state.hasLamp);
|
||||
lamp.classList.toggle('on', !!state.lamp);
|
||||
}
|
||||
@@ -70,7 +159,9 @@ window.addEventListener('message', (event) => {
|
||||
// Damit sich das HUD im Browser ohne FiveM anschauen laesst.
|
||||
if (typeof GetParentResourceName !== 'function') {
|
||||
render({
|
||||
visible: true, oxygen: 372, capacity: 600, depth: 12.4, pressure: 186,
|
||||
visible: true, oxygen: 372, capacity: 600,
|
||||
depth: 27.5, depthScale: 100, depthLimit: 45, deep: true,
|
||||
pressure: 186, maxPressure: 300, reservePressure: 50,
|
||||
level: 'ok', hasLamp: true, lamp: true
|
||||
});
|
||||
}
|
||||
|
||||
+126
-72
@@ -8,7 +8,8 @@
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
--panel: rgba(9, 18, 28, 0.72);
|
||||
--ring: 327; /* 2 * PI * 52, Umfang des Gauge-Kreises */
|
||||
--dial-face: #0b1622;
|
||||
--ring: 327; /* 2 * PI * 52, Umfang des Luft-Rings */
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -24,9 +25,9 @@ body {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Grid statt Flex: so sitzen Tiefe, Ring und Druck garantiert auf einer
|
||||
gemeinsamen Mittelachse, und die Lampen-Leiste hängt in einer zweiten Zeile
|
||||
mittig unter dem Ring - ohne Ausgleichs-Margins. */
|
||||
/* Dreier-Konsole: Finimeter, Luft, Tiefenmesser nebeneinander, Lampe darunter.
|
||||
Grid statt Flex, damit die drei Instrumente auf einer Mittelachse sitzen und
|
||||
die Lampen-Leiste ohne Ausgleichs-Margins in Zeile 2 haengt. */
|
||||
#hud {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -37,9 +38,9 @@ body {
|
||||
grid-template-rows: auto auto;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
column-gap: 22px;
|
||||
row-gap: 8px;
|
||||
padding: 14px 22px;
|
||||
column-gap: 20px;
|
||||
row-gap: 6px;
|
||||
padding: 14px 20px;
|
||||
border-radius: 16px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
@@ -54,7 +55,119 @@ body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- Luft-Gauge --- */
|
||||
.caption {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.4px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* --- Zeigerinstrumente --- */
|
||||
|
||||
.dial {
|
||||
grid-row: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#pressure { grid-column: 1; }
|
||||
#depth { grid-column: 3; }
|
||||
|
||||
.dial-face {
|
||||
position: relative;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.dial-face svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dial .face {
|
||||
fill: var(--dial-face);
|
||||
stroke: rgba(148, 163, 184, 0.25);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
/* Skalenzahlen gibt es bewusst nicht: bei dieser Groesse liest die niemand.
|
||||
Der Zeiger zeigt die Tendenz, die Zahl in der Mitte den genauen Wert. */
|
||||
.dial .ticks line {
|
||||
stroke: #cbd5e1;
|
||||
stroke-opacity: 0.4;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.dial .ticks line.major {
|
||||
stroke-opacity: 0.85;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
|
||||
.dial .zone {
|
||||
fill: none;
|
||||
stroke: var(--danger);
|
||||
stroke-width: 3.5;
|
||||
stroke-opacity: 0.9;
|
||||
}
|
||||
|
||||
.dial .needle line {
|
||||
stroke: var(--text);
|
||||
stroke-width: 2.6;
|
||||
stroke-linecap: round;
|
||||
transition: stroke 260ms ease;
|
||||
}
|
||||
|
||||
.dial .needle circle {
|
||||
fill: var(--text);
|
||||
transition: fill 260ms ease;
|
||||
}
|
||||
|
||||
/* Kein transform-origin hier: das rotate(winkel 50 50) im transform-Attribut
|
||||
bringt den Drehpunkt schon mit. Beides zusammen verschiebt den Zeiger aus der
|
||||
Uhr heraus - CSS rechnet die Origin nochmal obendrauf. */
|
||||
.dial .needle {
|
||||
transition: transform 260ms ease-out;
|
||||
}
|
||||
|
||||
.dial-value {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
/* In den offenen unteren Teil der Skala, damit die Zahl nicht am
|
||||
Zeiger-Drehpunkt klebt - wie das Sichtfenster eines echten Instruments. */
|
||||
padding-top: 36px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
transition: color 260ms ease;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Zeiger und Wert faerben sich, sobald der rote Bereich erreicht ist. */
|
||||
#pressure.alert .needle line,
|
||||
#depth.alert .needle line { stroke: var(--danger); }
|
||||
#pressure.alert .needle circle,
|
||||
#depth.alert .needle circle { fill: var(--danger); }
|
||||
#pressure.alert .dial-value,
|
||||
#depth.alert .dial-value { color: var(--danger); }
|
||||
|
||||
#depth.deep .dial-value { color: var(--warn); }
|
||||
#depth.deep .needle line { stroke: var(--warn); }
|
||||
#depth.deep .needle circle { fill: var(--warn); }
|
||||
|
||||
/* --- Luft-Ring --- */
|
||||
|
||||
.gauge {
|
||||
position: relative;
|
||||
@@ -105,56 +218,7 @@ body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1.4px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* --- Tiefe + Lampe --- */
|
||||
|
||||
.readout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 84px;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
/* Tiefe spiegelt den Druck: Werte zeigen jeweils zum Ring hin. */
|
||||
.readout--right {
|
||||
align-items: flex-end;
|
||||
grid-column: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.readout--right .value {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#pressure {
|
||||
grid-column: 3;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 3px;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
transition: color 260ms ease;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
/* --- Lampe --- */
|
||||
|
||||
.lamp {
|
||||
grid-column: 2;
|
||||
@@ -179,16 +243,14 @@ body {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.lamp.hidden {
|
||||
display: none;
|
||||
}
|
||||
.lamp.hidden { display: none; }
|
||||
|
||||
.lamp.on {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* --- Warnzustaende --- */
|
||||
/* --- Warnzustaende der Restluft --- */
|
||||
|
||||
#hud.warn .gauge-fill { stroke: var(--warn); }
|
||||
#hud.warn #air-value { color: var(--warn); }
|
||||
@@ -197,16 +259,8 @@ body {
|
||||
#hud.danger #air-value { color: var(--danger); }
|
||||
#hud.danger .gauge { animation: pulse 1s ease-in-out infinite; }
|
||||
|
||||
#hud.deep #depth-value { color: var(--warn); }
|
||||
|
||||
/* Tiefengrenze des Atemgases überschritten - Tiefenrausch läuft. */
|
||||
#hud.overlimit #depth-value {
|
||||
color: var(--danger);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Reservedruck: in der Tauchpraxis die Schwelle zum Aufstieg. */
|
||||
#pressure.reserve .value { color: var(--danger); }
|
||||
/* Tiefengrenze des Atemgases ueberschritten - Tiefenrausch laeuft. */
|
||||
#hud.overlimit #depth .dial-face { animation: pulse 1s ease-in-out infinite; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
|
||||
Reference in New Issue
Block a user