@sailingrotevista/rotevista-dash 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +135 -56
- package/index.html +4 -1
- package/package.json +1 -1
- package/style.css +34 -8
package/app.js
CHANGED
|
@@ -42,6 +42,9 @@ let socket, renderInterval, simInterval;
|
|
|
42
42
|
let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
|
|
43
43
|
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
44
44
|
|
|
45
|
+
// Variabili di stato per filtri e trend
|
|
46
|
+
let smoothedLeeway = 0;
|
|
47
|
+
let rotationTrend = 0;
|
|
45
48
|
let pressTimer, isFocusActive = false, blockNextClick = false;
|
|
46
49
|
|
|
47
50
|
const graphModes = {
|
|
@@ -137,8 +140,41 @@ function processIncomingData(path, val) {
|
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
// ==========================================================================
|
|
140
|
-
// 5. MOTORE RENDERING PRINCIPALE
|
|
143
|
+
// 5. MOTORE RENDERING PRINCIPALE E TREND
|
|
141
144
|
// ==========================================================================
|
|
145
|
+
|
|
146
|
+
// Calcolo Trend del Vento (Definita prima del loop per evitare ReferenceError)
|
|
147
|
+
function updateWindTrend() {
|
|
148
|
+
const longAvg = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
|
|
149
|
+
const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 15000, false);
|
|
150
|
+
|
|
151
|
+
if (!longAvg || !shortAvg) return;
|
|
152
|
+
|
|
153
|
+
let diff = (shortAvg.val - longAvg.val + 540) % 360 - 180;
|
|
154
|
+
|
|
155
|
+
// Smoothing del trend per evitare scatti
|
|
156
|
+
rotationTrend = (rotationTrend * 0.95) + (diff * 0.05);
|
|
157
|
+
|
|
158
|
+
// Seleziona sia i pallini della bussola che del gauge centrale
|
|
159
|
+
const cwDots = [document.getElementById('trend-dot-cw'), document.getElementById('trend-gauge-cw')];
|
|
160
|
+
const ccwDots = [document.getElementById('trend-dot-ccw'), document.getElementById('trend-gauge-ccw')];
|
|
161
|
+
|
|
162
|
+
// Soglia: il vento deve ruotare di almeno 1.5 gradi di media per attivarsi
|
|
163
|
+
if (Math.abs(rotationTrend) > 1.5) {
|
|
164
|
+
if (rotationTrend > 0) { // Orario
|
|
165
|
+
cwDots.forEach(el => el && el.classList.add('is-trending'));
|
|
166
|
+
ccwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
167
|
+
} else { // Antiorario
|
|
168
|
+
ccwDots.forEach(el => el && el.classList.add('is-trending'));
|
|
169
|
+
cwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// Vento stabile, spegni tutti i pallini
|
|
173
|
+
cwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
174
|
+
ccwDots.forEach(el => el && el.classList.remove('is-trending'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
142
178
|
function startDisplayLoop() {
|
|
143
179
|
renderInterval = setInterval(() => {
|
|
144
180
|
const now = Date.now();
|
|
@@ -149,7 +185,7 @@ function startDisplayLoop() {
|
|
|
149
185
|
let curSog = 0; if (store.raw["navigation.speedOverGround"] !== undefined) { curSog = msToKts(store.raw["navigation.speedOverGround"]); ui.sog.innerText = curSog.toFixed(1); manageHistory('sog', curSog); }
|
|
150
186
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) { const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d); }
|
|
151
187
|
|
|
152
|
-
// --- 1. RENDERING TWS CON COLORE DINAMICO
|
|
188
|
+
// --- 1. RENDERING TWS CON COLORE DINAMICO ---
|
|
153
189
|
if (store.raw["environment.wind.speedTrue"] !== undefined) {
|
|
154
190
|
const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
|
|
155
191
|
ui.tws.innerText = twsKts.toFixed(1);
|
|
@@ -167,12 +203,23 @@ function startDisplayLoop() {
|
|
|
167
203
|
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, CONFIG.averages.smoothWindow, true);
|
|
168
204
|
if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, smTwa.val); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
|
|
169
205
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
206
|
+
// --- CALCOLO E SMUSSAMENTO LEEWAY ---
|
|
207
|
+
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
208
|
+
let rawDrift = (radToDeg(store.raw["navigation.courseOverGroundTrue"]) - radToDeg(store.raw["navigation.headingTrue"]) + 360) % 360;
|
|
209
|
+
if (rawDrift > 180) rawDrift -= 360;
|
|
210
|
+
|
|
211
|
+
if (curSog < CONFIG.averages.minSpeed) {
|
|
212
|
+
smoothedLeeway = 0;
|
|
213
|
+
} else {
|
|
214
|
+
smoothedLeeway = (smoothedLeeway * 0.9) + (rawDrift * 0.1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
218
|
+
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
219
|
+
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
220
|
+
} else {
|
|
221
|
+
updateLeewayDisplay(0);
|
|
222
|
+
}
|
|
176
223
|
|
|
177
224
|
// --- 2. RENDERING MEDIE E BUSSOLA TATTICA ---
|
|
178
225
|
if (now - lastAvgUIUpdate > 3000) {
|
|
@@ -182,14 +229,29 @@ function startDisplayLoop() {
|
|
|
182
229
|
twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averages.longWindow, true),
|
|
183
230
|
twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averages.longWindow, false);
|
|
184
231
|
|
|
185
|
-
const upUI = (el, obj) => {
|
|
186
|
-
if (!obj
|
|
187
|
-
|
|
188
|
-
el.
|
|
189
|
-
|
|
232
|
+
const upUI = (el, obj, isCompass = false) => {
|
|
233
|
+
if (!obj || obj.val === null) {
|
|
234
|
+
el.innerHTML = "---°";
|
|
235
|
+
el.classList.remove('unstable-data');
|
|
236
|
+
} else {
|
|
237
|
+
let val = Math.round(obj.val);
|
|
238
|
+
let displayVal;
|
|
239
|
+
|
|
240
|
+
if (isCompass) displayVal = ((val + 360) % 360).toString().padStart(3, '0');
|
|
241
|
+
else displayVal = val.toString();
|
|
242
|
+
|
|
243
|
+
el.innerHTML = `${displayVal}°`;
|
|
244
|
+
|
|
245
|
+
if (obj.stable || curSog < CONFIG.averages.minSpeed) el.classList.remove('unstable-data');
|
|
246
|
+
else el.classList.add('unstable-data');
|
|
190
247
|
}
|
|
191
248
|
};
|
|
192
|
-
|
|
249
|
+
|
|
250
|
+
upUI(ui.hdg, hObj, true);
|
|
251
|
+
upUI(ui.cog, cObj, true);
|
|
252
|
+
upUI(ui.awaAvg, awObj, false);
|
|
253
|
+
upUI(ui.twaAvg, twObj, false);
|
|
254
|
+
upUI(ui.twdAvg, twdObj, true);
|
|
193
255
|
|
|
194
256
|
if (hObj && twObj && hObj.val !== null) {
|
|
195
257
|
let tA = twObj.val * 2; ui.tackHdg.innerHTML = `${Math.round((hObj.val - tA + 360) % 360).toString().padStart(3, '0')}°`;
|
|
@@ -201,6 +263,8 @@ function startDisplayLoop() {
|
|
|
201
263
|
|
|
202
264
|
// Rotazione Bussola Tattica e Colore Sincronizzato
|
|
203
265
|
if (twdObj && hObj) {
|
|
266
|
+
updateWindTrend(); // Aggiorna i pallini
|
|
267
|
+
|
|
204
268
|
curTwdRoseRot = getShortestRotation(curTwdRoseRot, twdObj.val);
|
|
205
269
|
ui.twdArrow.setAttribute('transform', `rotate(${curTwdRoseRot}, 20, 20)`);
|
|
206
270
|
ui.twdBoat.setAttribute('transform', `rotate(${hObj.val}, 20, 20)`);
|
|
@@ -338,16 +402,14 @@ function toggleFocusMode(type, element) {
|
|
|
338
402
|
// ==========================================================================
|
|
339
403
|
if (ui.hotspot) {
|
|
340
404
|
let pressTimer;
|
|
341
|
-
const HOLD_DURATION = 1000;
|
|
405
|
+
const HOLD_DURATION = 1000;
|
|
342
406
|
|
|
343
407
|
ui.hotspot.addEventListener('pointerdown', (e) => {
|
|
344
|
-
// Avvia il timer al tocco
|
|
345
408
|
pressTimer = setTimeout(() => {
|
|
346
409
|
document.body.classList.toggle('night-mode');
|
|
347
|
-
// Feedback visivo immediato al cambio modalità
|
|
348
410
|
ui.hotspot.style.opacity = "0.5";
|
|
349
411
|
setTimeout(() => ui.hotspot.style.opacity = "1", 200);
|
|
350
|
-
pressTimer = null;
|
|
412
|
+
pressTimer = null;
|
|
351
413
|
}, HOLD_DURATION);
|
|
352
414
|
});
|
|
353
415
|
|
|
@@ -355,7 +417,6 @@ if (ui.hotspot) {
|
|
|
355
417
|
if (pressTimer) {
|
|
356
418
|
clearTimeout(pressTimer);
|
|
357
419
|
pressTimer = null;
|
|
358
|
-
// Se arriviamo qui, è stato un click rapido -> Fullscreen
|
|
359
420
|
const doc = document.documentElement;
|
|
360
421
|
const isF = document.fullscreenElement || document.webkitFullscreenElement;
|
|
361
422
|
if (!isF) {
|
|
@@ -382,7 +443,9 @@ window.addEventListener('load', init);
|
|
|
382
443
|
function checkDepthAlarm(m) { ui.depth.classList.remove('alarm-warning', 'alarm-danger'); if (m < CONFIG.alarms.depthDanger) { ui.depth.classList.add('alarm-danger'); playBingBing(); } else if (m < CONFIG.alarms.depthWarning) ui.depth.classList.add('alarm-warning'); }
|
|
383
444
|
function playBingBing() { if (!audioCtx) return; const n = Date.now(); if (n - lastAlarmTime < 3000) return; lastAlarmTime = n; function b(f, s) { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination); o.frequency.value = f; g.gain.setValueAtTime(0.1, s); g.gain.exponentialRampToValueAtTime(0.01, s + 0.4); o.start(s); o.stop(s + 0.5); } b(880, audioCtx.currentTime); b(880, audioCtx.currentTime + 0.6); }
|
|
384
445
|
|
|
385
|
-
//
|
|
446
|
+
// ==========================================================================
|
|
447
|
+
// 9. MOTORE SIMULAZIONE DINAMICA E AVVIO (3 Click su Depth)
|
|
448
|
+
// ==========================================================================
|
|
386
449
|
ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
387
450
|
let dC = 0, lC = 0;
|
|
388
451
|
return function() {
|
|
@@ -393,7 +456,7 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
|
393
456
|
simulationMode = !simulationMode;
|
|
394
457
|
if (simulationMode) {
|
|
395
458
|
if (socket) socket.close();
|
|
396
|
-
startDynamicSimulation();
|
|
459
|
+
startDynamicSimulation();
|
|
397
460
|
} else {
|
|
398
461
|
location.reload();
|
|
399
462
|
}
|
|
@@ -402,46 +465,62 @@ ui.depth.closest('.data-box').addEventListener('click', (function() {
|
|
|
402
465
|
};
|
|
403
466
|
})());
|
|
404
467
|
|
|
405
|
-
// ==========================================================================
|
|
406
|
-
// 9. MOTORE SIMULAZIONE DINAMICA
|
|
407
|
-
// ==========================================================================
|
|
408
468
|
function startDynamicSimulation() {
|
|
409
469
|
ui.status.innerText = "SIM ATTIVO";
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
470
|
+
|
|
471
|
+
let sim = {
|
|
472
|
+
hdg: Math.random() * 360,
|
|
473
|
+
tws: 12,
|
|
474
|
+
twd: Math.random() * 360,
|
|
475
|
+
depth: 12,
|
|
476
|
+
stw: 5,
|
|
477
|
+
leeway: 0,
|
|
478
|
+
startTime: Date.now()
|
|
479
|
+
};
|
|
480
|
+
|
|
414
481
|
simInterval = setInterval(() => {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const stw = Math.min(tws * 0.7, 8); // Velocità barca legata al vento
|
|
426
|
-
const twaRad = degToRad(twa);
|
|
427
|
-
|
|
428
|
-
// AWS = radq(stw² + tws² + 2*stw*tws*cos(twa))
|
|
429
|
-
const aws = Math.sqrt(Math.pow(stw, 2) + Math.pow(tws, 2) + 2 * stw * tws * Math.cos(twaRad));
|
|
430
|
-
// AWA = atan2(tws*sin(twa), stw + tws*cos(twa))
|
|
431
|
-
const awa = radToDeg(Math.atan2(tws * Math.sin(twaRad), stw + tws * Math.cos(twaRad)));
|
|
432
|
-
|
|
433
|
-
// Calcolo Leeway (Scarroccio): più forte il vento, più scarroccia
|
|
434
|
-
const leeway = (tws > 5) ? Math.sin(twaRad) * (tws * 0.2) : 0;
|
|
482
|
+
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
483
|
+
|
|
484
|
+
// Vento: Rotazione lenta + Salto netto a 120s
|
|
485
|
+
if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
|
|
486
|
+
sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
|
|
487
|
+
|
|
488
|
+
// Raffica
|
|
489
|
+
const inGust = (elapsed % 40) < 5;
|
|
490
|
+
const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
|
|
491
|
+
sim.tws += (targetTws - sim.tws) * 0.05;
|
|
435
492
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
493
|
+
// Barca segue il vento pigramente
|
|
494
|
+
sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
|
|
495
|
+
|
|
496
|
+
let twaRel = (sim.twd - sim.hdg + 360) % 360;
|
|
497
|
+
if (twaRel > 180) twaRel -= 360;
|
|
498
|
+
|
|
499
|
+
// Polare smussata
|
|
500
|
+
let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125));
|
|
501
|
+
sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
|
|
502
|
+
|
|
503
|
+
// Leeway smussato
|
|
504
|
+
const rawLeeway = Math.sin(degToRad(twaRel)) * 4;
|
|
505
|
+
sim.leeway += (rawLeeway - sim.leeway) * 0.05;
|
|
506
|
+
|
|
507
|
+
// Vettori Reali
|
|
508
|
+
const cog = (sim.hdg - sim.leeway + 360) % 360;
|
|
509
|
+
const twaRad = degToRad(twaRel);
|
|
510
|
+
const aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
|
|
511
|
+
const awa = radToDeg(Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad)));
|
|
512
|
+
|
|
513
|
+
// Invio Dati (Con tutto il set necessario per la UI)
|
|
514
|
+
processIncomingData("environment.wind.speedTrue", ktsToMs(sim.tws));
|
|
515
|
+
processIncomingData("environment.wind.directionTrue", degToRad(sim.twd));
|
|
516
|
+
processIncomingData("environment.wind.angleTrueWater", degToRad(twaRel));
|
|
439
517
|
processIncomingData("environment.wind.speedApparent", ktsToMs(aws));
|
|
440
518
|
processIncomingData("environment.wind.angleApparent", degToRad(awa));
|
|
441
|
-
processIncomingData("environment.depth.belowTransducer", depth);
|
|
442
|
-
processIncomingData("navigation.headingTrue", degToRad(
|
|
443
|
-
processIncomingData("navigation.speedThroughWater", ktsToMs(stw));
|
|
444
|
-
processIncomingData("navigation.
|
|
519
|
+
processIncomingData("environment.depth.belowTransducer", sim.depth);
|
|
520
|
+
processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
|
|
521
|
+
processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw));
|
|
522
|
+
processIncomingData("navigation.speedOverGround", ktsToMs(sim.stw));
|
|
523
|
+
processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
|
|
524
|
+
|
|
445
525
|
}, 1000);
|
|
446
526
|
}
|
|
447
|
-
|
package/index.html
CHANGED
|
@@ -179,6 +179,8 @@
|
|
|
179
179
|
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.85">
|
|
180
180
|
<path d="M200,90 L206,98 L200,125 L194,98 Z" fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
181
181
|
<text x="200" y="104" fill="#000" font-size="8" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
182
|
+
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#ff0000" opacity="0" />
|
|
183
|
+
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#00ff00" opacity="0" />
|
|
182
184
|
</g>
|
|
183
185
|
|
|
184
186
|
<!-- Barra LEEWAY / SCARROCCIO Inferiore -->
|
|
@@ -282,7 +284,6 @@
|
|
|
282
284
|
<path d="M 20,17 L 17,26 L 20,24.5 L 23,26 Z" fill="white" opacity="0.2" />
|
|
283
285
|
</g>
|
|
284
286
|
|
|
285
|
-
<!-- 2. INDICATORE VENTO: Ruota con il TWD (id: twd-arrow) -->
|
|
286
287
|
<g id="twd-arrow" transform="rotate(0, 20, 20)">
|
|
287
288
|
<path id="twd-wind-chevron" d="M 17,5 L 20,7.5 L 23,5"
|
|
288
289
|
fill="none"
|
|
@@ -290,6 +291,8 @@
|
|
|
290
291
|
stroke-width="2.2"
|
|
291
292
|
stroke-linecap="round"
|
|
292
293
|
stroke-linejoin="round" />
|
|
294
|
+
<circle id="trend-dot-cw" cx="24" cy="7.5" r="1.5" fill="#ff0000" />
|
|
295
|
+
<circle id="trend-dot-ccw" cx="16" cy="7.5" r="1.5" fill="#00ff00" />
|
|
293
296
|
</g>
|
|
294
297
|
</svg>
|
|
295
298
|
<span class="value value-large" id="twd-avg">---°</span>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -77,7 +77,7 @@ body {
|
|
|
77
77
|
.data-box {
|
|
78
78
|
position: relative;
|
|
79
79
|
border-bottom: 1px solid #222;
|
|
80
|
-
padding: 8px
|
|
80
|
+
padding: 4px 8px;
|
|
81
81
|
display: flex;
|
|
82
82
|
flex-direction: column;
|
|
83
83
|
width: 100%;
|
|
@@ -91,9 +91,18 @@ body {
|
|
|
91
91
|
.left-panel .data-box { align-items: flex-start; text-align: left; }
|
|
92
92
|
.right-panel .data-box { align-items: flex-end; text-align: right; }
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
.side-panel {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
height: 100%;
|
|
98
|
+
gap: 2px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.data-box {
|
|
102
|
+
flex: 1 1 0px; /* Ogni box si divide lo spazio equamente, ma può ridursi se serve */
|
|
103
|
+
min-height: 0; /* Fondamentale per permettere al grafico di non eccedere */
|
|
104
|
+
padding: 2px 4px; /* Ancora più compatto */
|
|
105
|
+
}
|
|
97
106
|
|
|
98
107
|
/* ==========================================================================
|
|
99
108
|
3. TACTICAL FOCUS MODE (AUTO-EXPANDING SPLIT)
|
|
@@ -208,20 +217,20 @@ body {
|
|
|
208
217
|
.graph-wrapper {
|
|
209
218
|
position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
|
|
210
219
|
display: flex; align-items: stretch; background: rgba(255, 255, 255, 0.03);
|
|
211
|
-
border-radius: 4px; gap:
|
|
220
|
+
border-radius: 4px; gap: 0px !important;
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
/* Spostamento verso il centro per eliminare spazi neri morti */
|
|
215
224
|
.left-panel .graph-wrapper { margin-right: -6px; }
|
|
216
225
|
.right-panel .graph-wrapper { margin-left: -6px; }
|
|
217
226
|
|
|
218
|
-
.sparkline { flex-grow: 1; height: 100%; background: transparent !important; display: block; }
|
|
227
|
+
.sparkline { flex-grow: 1; height: 100%; width: 100% !important; background: transparent !important; display: block; }
|
|
219
228
|
.sparkline path, .sparkline line { stroke-width: 1px !important; transition: all 0.3s ease; }
|
|
220
229
|
|
|
221
230
|
.scale-labels {
|
|
222
231
|
display: flex; flex-direction: column; justify-content: space-between;
|
|
223
|
-
font-size: 10px; color: #777; font-weight: bold; min-width:
|
|
224
|
-
height: 100%; line-height: 1; padding:
|
|
232
|
+
font-size: 10px; color: #777; font-weight: bold; min-width: 12px;
|
|
233
|
+
height: 100%; line-height: 1; padding: 0 px 0; border: none !important;
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
/* Simmetria: le scale numeriche "guardano" sempre il quadrante centrale */
|
|
@@ -452,3 +461,20 @@ body.night-mode {
|
|
|
452
461
|
fill: #ff3333 !important;
|
|
453
462
|
opacity: 0.4 !important;
|
|
454
463
|
}
|
|
464
|
+
|
|
465
|
+
/* Definiamo il lampeggio */
|
|
466
|
+
@keyframes blink-trend {
|
|
467
|
+
0%, 100% { opacity: 1; }
|
|
468
|
+
50% { opacity: 0; }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Stile base dei pallini */
|
|
472
|
+
#trend-dot-cw, #trend-dot-ccw, #trend-gauge-cw, #trend-gauge-ccw {
|
|
473
|
+
opacity: 0.3;
|
|
474
|
+
transition: opacity 0.3s ease;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* Quando attivi la classe, forziamo l'animazione */
|
|
478
|
+
.is-trending {
|
|
479
|
+
animation: blink-trend 1s infinite !important;
|
|
480
|
+
}
|