@sailingrotevista/rotevista-dash 6.2.8 → 7.0.1
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 +107 -586
- package/charts.js +274 -0
- package/gauge.js +81 -0
- package/index.html +3 -1
- package/package.json +1 -1
- package/radar.html +183 -74
- package/utils.js +139 -0
package/radar.html
CHANGED
|
@@ -112,18 +112,28 @@
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
.status-badge {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
display: inline-block;
|
|
116
|
+
padding: 2px 6px;
|
|
117
|
+
border-radius: 3px;
|
|
118
|
+
font-weight: bold;
|
|
119
|
+
color: #fff;
|
|
120
|
+
text-transform: uppercase;
|
|
121
|
+
font-size: 9px;
|
|
122
|
+
}
|
|
123
|
+
.status-online { background: #00C851; }
|
|
124
|
+
.status-offline { background: #ff3b30; }
|
|
125
|
+
|
|
126
|
+
/* Chirurgico: Animazione e stile per la punta infuocata del trend meteo */
|
|
127
|
+
@keyframes blink-trend {
|
|
128
|
+
0%, 100% { opacity: 1; filter: drop-shadow(0 0 6px currentColor); }
|
|
129
|
+
50% { opacity: 0.3; filter: drop-shadow(0 0 1px currentColor); }
|
|
130
|
+
}
|
|
131
|
+
.is-trending {
|
|
132
|
+
animation: blink-trend 1.2s infinite ease-in-out !important;
|
|
133
|
+
transform-origin: center;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
127
137
|
<body>
|
|
128
138
|
|
|
129
139
|
<!-- AREA SUPERIORE: BUSSOLA RADAR TATTICA -->
|
|
@@ -251,10 +261,74 @@
|
|
|
251
261
|
const BORDER_STROKE_WIDTH = ARC_STROKE_WIDTH + 2; // 7px
|
|
252
262
|
|
|
253
263
|
const radToDeg = (rad) => (rad * 180 / Math.PI);
|
|
254
|
-
|
|
255
|
-
|
|
264
|
+
const degToRad = (deg) => (deg * Math.PI / 180);
|
|
265
|
+
const msToKts = (ms) => ms * 1.94384;
|
|
266
|
+
|
|
267
|
+
// Chirurgico: Inserita la funzione vettoriale circolare con calcolo trigonometrico al volo per l'analisi dei trend meteo
|
|
268
|
+
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
|
|
269
|
+
now = now || Date.now();
|
|
270
|
+
const len = bufferArray.length;
|
|
271
|
+
if (len === 0) return null;
|
|
272
|
+
|
|
273
|
+
let sSin = 0, sCos = 0, count = 0;
|
|
274
|
+
let newestTime = 0, oldestTime = 0;
|
|
275
|
+
|
|
276
|
+
let pilotSin = 0, pilotCos = 0;
|
|
277
|
+
const pilotSamples = Math.min(len, 15);
|
|
278
|
+
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
279
|
+
const val = bufferArray[i].val;
|
|
280
|
+
pilotSin += Math.sin(val);
|
|
281
|
+
pilotCos += Math.cos(val);
|
|
282
|
+
}
|
|
283
|
+
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
284
|
+
const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
|
|
285
|
+
|
|
286
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
287
|
+
const item = bufferArray[i];
|
|
288
|
+
if ((now - item.time) > windowMs) break;
|
|
289
|
+
|
|
290
|
+
const itemVal = item.val;
|
|
291
|
+
const itemSin = Math.sin(itemVal);
|
|
292
|
+
const itemCos = Math.cos(itemVal);
|
|
293
|
+
|
|
294
|
+
let diffRad = Math.atan2(Math.sin(itemVal - pilotRad), Math.cos(itemVal - pilotRad));
|
|
295
|
+
let finalSin, finalCos;
|
|
296
|
+
|
|
297
|
+
if (Math.abs(diffRad) > limitRad) {
|
|
298
|
+
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
299
|
+
const clampedRad = pilotRad + clampedDiff;
|
|
300
|
+
finalSin = Math.sin(clampedRad);
|
|
301
|
+
finalCos = Math.cos(clampedRad);
|
|
302
|
+
} else {
|
|
303
|
+
finalSin = itemSin;
|
|
304
|
+
finalCos = itemCos;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
sSin += finalSin;
|
|
308
|
+
sCos += finalCos;
|
|
309
|
+
|
|
310
|
+
if (count === 0) newestTime = item.time;
|
|
311
|
+
oldestTime = item.time;
|
|
312
|
+
count++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (count === 0) return null;
|
|
316
|
+
|
|
317
|
+
const R = Math.hypot(sSin, sCos) / count;
|
|
318
|
+
const avgRad = Math.atan2(sSin, sCos);
|
|
319
|
+
const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
320
|
+
const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
|
|
321
|
+
const safeR = Math.max(R, 1e-9);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
val: finalVal,
|
|
325
|
+
stable: historyDuration > 10000 && R > (CONFIG.averaging.stabilityThreshold || 0.95),
|
|
326
|
+
dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
|
|
327
|
+
samples: count
|
|
328
|
+
};
|
|
329
|
+
}
|
|
256
330
|
|
|
257
|
-
|
|
331
|
+
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
|
|
258
332
|
const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
|
|
259
333
|
return {
|
|
260
334
|
x: centerX + (radius * Math.cos(angleInRadians)),
|
|
@@ -278,51 +352,56 @@
|
|
|
278
352
|
}
|
|
279
353
|
|
|
280
354
|
// --- 2. GESTIONE SEGNALI IN TEMPO REALE ---
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (path === 'navigation.speedThroughWater') document.getElementById('debug-stw').innerText = msToKts(val).toFixed(1);
|
|
287
|
-
else if (path === 'navigation.speedOverGround') document.getElementById('debug-sog').innerText = msToKts(val).toFixed(1);
|
|
288
|
-
else if (path === 'navigation.headingTrue') document.getElementById('debug-hdg').innerText = Math.round(radToDeg(val)) + '°';
|
|
289
|
-
else if (path === 'navigation.courseOverGroundTrue') document.getElementById('debug-cog').innerText = Math.round(radToDeg(val)) + '°';
|
|
290
|
-
else if (path === 'environment.wind.speedApparent') document.getElementById('debug-aws').innerText = msToKts(val).toFixed(1);
|
|
291
|
-
else if (path === 'environment.wind.angleApparent') document.getElementById('debug-awa').innerText = Math.round(radToDeg(val)) + '°';
|
|
292
|
-
else if (path === 'navigation.position') {
|
|
293
|
-
document.getElementById('debug-gps').innerText = val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4);
|
|
294
|
-
}
|
|
355
|
+
function setDebugText(id, text) {
|
|
356
|
+
const el = document.getElementById(id);
|
|
357
|
+
if (el) el.innerText = text;
|
|
358
|
+
}
|
|
295
359
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
360
|
+
function processIncomingDelta(path, val, source, timeMs) {
|
|
361
|
+
const now = timeMs || Date.now();
|
|
362
|
+
store.timestamps[path] = now;
|
|
363
|
+
store.raw[path] = val;
|
|
364
|
+
|
|
365
|
+
if (path === 'navigation.speedThroughWater') setDebugText('debug-stw', msToKts(val).toFixed(1));
|
|
366
|
+
else if (path === 'navigation.speedOverGround') setDebugText('debug-sog', msToKts(val).toFixed(1));
|
|
367
|
+
else if (path === 'navigation.headingTrue') setDebugText('debug-hdg', Math.round(radToDeg(val)) + '°');
|
|
368
|
+
else if (path === 'navigation.courseOverGroundTrue') setDebugText('debug-cog', Math.round(radToDeg(val)) + '°');
|
|
369
|
+
else if (path === 'environment.wind.speedApparent') setDebugText('debug-aws', msToKts(val).toFixed(1));
|
|
370
|
+
else if (path === 'environment.wind.angleApparent') setDebugText('debug-awa', Math.round(radToDeg(val)) + '°');
|
|
371
|
+
else if (path === 'navigation.position') {
|
|
372
|
+
setDebugText('debug-gps', val.latitude.toFixed(4) + '; ' + val.longitude.toFixed(4));
|
|
373
|
+
}
|
|
302
374
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
375
|
+
const aws = store.raw["environment.wind.speedApparent"];
|
|
376
|
+
const awa = store.raw["environment.wind.angleApparent"];
|
|
377
|
+
if (aws !== undefined && awa !== undefined) {
|
|
378
|
+
const stw = store.raw["navigation.speedThroughWater"] || 0;
|
|
379
|
+
const awsKts = msToKts(aws);
|
|
380
|
+
const stwKts = msToKts(stw);
|
|
310
381
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
382
|
+
const tw_water_x = awsKts * Math.cos(awa) - stwKts;
|
|
383
|
+
const tw_water_y = awsKts * Math.sin(awa);
|
|
384
|
+
|
|
385
|
+
const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
|
|
386
|
+
const twa = Math.atan2(tw_water_y, tw_water_x);
|
|
387
|
+
const hdg = store.raw["navigation.headingTrue"] || 0;
|
|
388
|
+
const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
314
389
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
while(store.twdMinuteBuffer.length > 1800) store.twdMinuteBuffer.shift(); // conserva 30 minuti a 1Hz
|
|
390
|
+
setDebugText('debug-tws', tws.toFixed(1));
|
|
391
|
+
setDebugText('debug-twa', Math.round(radToDeg(twa)) + '°');
|
|
392
|
+
setDebugText('debug-twd', Math.round(radToDeg(twd)) + '°');
|
|
319
393
|
|
|
320
|
-
|
|
321
|
-
|
|
394
|
+
// --- AGGIORNAMENTO DATI ISTANTANEI PER L'ELABORAZIONE ---
|
|
395
|
+
// Manteniamo aggiornato lo stato raw istantaneo in memoria per i calcoli del radar...
|
|
396
|
+
store.raw["environment.wind.directionTrue"] = twd;
|
|
397
|
+
store.raw["environment.wind.speedTrue"] = tws;
|
|
322
398
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
399
|
+
// NOTA: Non popoliamo più i buffer locali a 1Hz. Il presente dei 30 minuti (Anello 1)
|
|
400
|
+
// viene gestito e sincronizzato direttamente dal server tramite fetchConfigAndHistory().
|
|
401
|
+
|
|
402
|
+
renderRadar(); // Rinfresco grafico istantaneo reattivo per gli indicatori/trend
|
|
403
|
+
}
|
|
404
|
+
}
|
|
326
405
|
|
|
327
406
|
// --- 3. RETE E SINCRONIZZAZIONE REST ---
|
|
328
407
|
function getApiUrl(path) {
|
|
@@ -750,28 +829,58 @@
|
|
|
750
829
|
mainPath.setAttribute("stroke-width", ARC_STROKE_WIDTH);
|
|
751
830
|
mainPath.setAttribute("stroke-linecap", "round");
|
|
752
831
|
// Chirurgico: Per l'anello futuro, usiamo un'opacità morbida al 50% (0.5) continua (senza tratteggio) per un effetto "glowing" pulitissimo
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
832
|
+
mainPath.setAttribute("opacity", data.isFuture ? "0.5" : opacityValue);
|
|
833
|
+
mainPath.id = data.isFuture ? "" : (index === 1 ? "active-present-arc" : ""); // Identifichiamo l'Anello 1 per calcoli geometrici
|
|
834
|
+
ringsContainer.appendChild(mainPath);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// --- Chirurgico: Motore di calcolo e posizionamento LED dinamici sulle punte dell'Anello 1 ---
|
|
838
|
+
if (activeRing && !activeRing.isCalm) {
|
|
839
|
+
const twdNow = getCircularAverageFromBuffer(store.twdMinuteBuffer, 60000, false, now); // 1 minuto fisso
|
|
840
|
+
const isNavigating = (store.raw["navigation.speedThroughWater"] || 0) > 0.25 || (store.raw["navigation.speedOverGround"] || 0) > 0.25;
|
|
841
|
+
const strategicWindowMs = (isNavigating ? 15 : 60) * 60000;
|
|
842
|
+
const twdRef = getCircularAverageFromBuffer(store.twdMinuteBuffer, strategicWindowMs, false, now);
|
|
843
|
+
|
|
844
|
+
if (twdNow && twdRef) {
|
|
845
|
+
let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
846
|
+
|
|
847
|
+
if (Math.abs(deltaMeteo) > 6.0) {
|
|
848
|
+
const isSouth = store.raw["navigation.position"] && store.raw["navigation.position"].latitude < 0;
|
|
849
|
+
let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#00C851" : "#ff3b30") : (deltaMeteo > 0 ? "#00C851" : "#ff3b30");
|
|
850
|
+
|
|
851
|
+
// Calcoliamo la posizione geometrica esatta delle due punte dell'Anello 1 (raggio 67.2px)
|
|
852
|
+
const radiusAnello1 = ringRadii[1];
|
|
853
|
+
const angleTarget = deltaMeteo > 0 ? activeRing.twdMax : activeRing.twdMin; // Estremità destra (CW) o sinistra (CCW)
|
|
854
|
+
const pt = polarToCartesian(200, 200, radiusAnello1, angleTarget);
|
|
855
|
+
|
|
856
|
+
// Disegniamo il LED lampeggiante esattamente sulla punta dell'arco
|
|
857
|
+
const led = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
858
|
+
led.setAttribute("cx", pt.x.toFixed(1));
|
|
859
|
+
led.setAttribute("cy", pt.y.toFixed(1));
|
|
860
|
+
led.setAttribute("r", "5.5"); // Leggermente più spesso dell'arco per renderlo evidente
|
|
861
|
+
led.setAttribute("fill", meteoColor);
|
|
862
|
+
led.setAttribute("class", "is-trending"); // Attiva il lampeggio CSS
|
|
863
|
+
led.setAttribute("filter", "url(#center-glow)"); // Aggiunge un leggero bagliore visivo
|
|
864
|
+
ringsContainer.appendChild(led);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Aggiorna il pannello di debug ad ogni ridisegno del radar
|
|
870
|
+
updateDebugPanel();
|
|
871
|
+
}
|
|
760
872
|
|
|
761
873
|
// --- 6. CICLO DI VITA E AVVIO ---
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
window.onload = init;
|
|
774
|
-
|
|
874
|
+
function init() {
|
|
875
|
+
drawCompassTicks();
|
|
876
|
+
connect();
|
|
877
|
+
|
|
878
|
+
// Sincronizzazione strategica: scarica lo storico consolidato dal server ogni 15 secondi e ridisegna
|
|
879
|
+
setInterval(async () => {
|
|
880
|
+
await fetchConfigAndHistory();
|
|
881
|
+
renderRadar();
|
|
882
|
+
}, 15000);
|
|
883
|
+
}
|
|
775
884
|
window.onload = init;
|
|
776
885
|
</script>
|
|
777
886
|
</body>
|
package/utils.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ==========================================================================
|
|
3
|
+
* Sailing Dashboard Pro - Math, Conversions & Audio Utilities
|
|
4
|
+
* ==========================================================================
|
|
5
|
+
* Raccoglie le funzioni pure di calcolo vettoriale e sintesi sonora.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// --- 1. CONVERSIONI STANDARD ---
|
|
9
|
+
const radToDeg = (rad) => rad * (180 / Math.PI);
|
|
10
|
+
const degToRad = (deg) => deg * (Math.PI / 180);
|
|
11
|
+
const msToKts = (ms) => ms * 1.94384;
|
|
12
|
+
const ktsToMs = (kts) => kts / 1.94384;
|
|
13
|
+
|
|
14
|
+
// Calcola la rotta di rotazione più breve tra due angoli (evita scatti a 360°)
|
|
15
|
+
function getShortestRotation(curr, target) {
|
|
16
|
+
let diff = (target - curr) % 360;
|
|
17
|
+
if (diff > 180) diff -= 360;
|
|
18
|
+
else if (diff < -180) diff += 360;
|
|
19
|
+
return curr + diff;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Scrive testo in sicurezza evitando reflow inutili nel DOM/SVG
|
|
23
|
+
function safeSetText(el, text) {
|
|
24
|
+
if (!el) return;
|
|
25
|
+
const isSVG = el instanceof SVGElement;
|
|
26
|
+
if (isSVG) {
|
|
27
|
+
if (el.textContent !== text) el.textContent = text;
|
|
28
|
+
} else {
|
|
29
|
+
if (el.innerHTML !== text) el.innerHTML = text;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- 2. MOTORE MATEMATICO: MEDIA CIRCOLARE VETTORIALE ---
|
|
34
|
+
function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now, stabilityThreshold = 0.95, stabilityBreakout = 15) {
|
|
35
|
+
now = now || Date.now();
|
|
36
|
+
const len = bufferArray.length;
|
|
37
|
+
if (len === 0) return null;
|
|
38
|
+
|
|
39
|
+
let sSin = 0, sCos = 0, count = 0;
|
|
40
|
+
let newestTime = 0, oldestTime = 0;
|
|
41
|
+
|
|
42
|
+
let pilotSin = 0, pilotCos = 0;
|
|
43
|
+
const pilotSamples = Math.min(len, 15);
|
|
44
|
+
for (let i = len - 1; i >= len - pilotSamples; i--) {
|
|
45
|
+
pilotSin += bufferArray[i].sin;
|
|
46
|
+
pilotCos += bufferArray[i].cos;
|
|
47
|
+
}
|
|
48
|
+
const pilotRad = Math.atan2(pilotSin, pilotCos);
|
|
49
|
+
const limitRad = stabilityBreakout * (Math.PI / 180);
|
|
50
|
+
|
|
51
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
52
|
+
const item = bufferArray[i];
|
|
53
|
+
if ((now - item.time) > windowMs) break;
|
|
54
|
+
|
|
55
|
+
let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
|
|
56
|
+
let finalSin, finalCos;
|
|
57
|
+
|
|
58
|
+
if (Math.abs(diffRad) > limitRad) {
|
|
59
|
+
const clampedDiff = Math.sign(diffRad) * limitRad;
|
|
60
|
+
const clampedRad = pilotRad + clampedDiff;
|
|
61
|
+
finalSin = Math.sin(clampedRad);
|
|
62
|
+
finalCos = Math.cos(clampedRad);
|
|
63
|
+
} else {
|
|
64
|
+
finalSin = item.sin;
|
|
65
|
+
finalCos = item.cos;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
sSin += finalSin;
|
|
69
|
+
sCos += finalCos;
|
|
70
|
+
|
|
71
|
+
if (count === 0) newestTime = item.time;
|
|
72
|
+
oldestTime = item.time;
|
|
73
|
+
count++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (count === 0) return null;
|
|
77
|
+
|
|
78
|
+
const R = Math.hypot(sSin, sCos) / count;
|
|
79
|
+
const avgRad = Math.atan2(sSin, sCos);
|
|
80
|
+
const finalVal = signed ? avgRad : (avgRad + Math.PI * 2) % (Math.PI * 2);
|
|
81
|
+
const historyDuration = (count > 2) ? (newestTime - oldestTime) : 0;
|
|
82
|
+
const safeR = Math.max(R, 1e-9);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
val: finalVal,
|
|
86
|
+
stable: historyDuration > 10000 && R > stabilityThreshold,
|
|
87
|
+
dev: (R < 1) ? Math.round(Math.sqrt(-2 * Math.log(safeR)) * (180 / Math.PI)) : 0,
|
|
88
|
+
samples: count
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- 3. SINTESI AUDIO WEB AUDIO API (ALLARMI ACUSTICI) ---
|
|
93
|
+
let audioCtx = null;
|
|
94
|
+
let lastAlarmTime = 0;
|
|
95
|
+
|
|
96
|
+
function playBingBing() {
|
|
97
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
98
|
+
const n = Date.now();
|
|
99
|
+
if (n - lastAlarmTime < 3000) return;
|
|
100
|
+
lastAlarmTime = n;
|
|
101
|
+
|
|
102
|
+
function b(f, s) {
|
|
103
|
+
const o = audioCtx.createOscillator();
|
|
104
|
+
const g = audioCtx.createGain();
|
|
105
|
+
o.connect(g);
|
|
106
|
+
g.connect(audioCtx.destination);
|
|
107
|
+
o.frequency.value = f;
|
|
108
|
+
g.gain.setValueAtTime(0.1, s);
|
|
109
|
+
g.gain.exponentialRampToValueAtTime(0.01, s + 0.4);
|
|
110
|
+
o.start(s);
|
|
111
|
+
o.stop(s + 0.5);
|
|
112
|
+
}
|
|
113
|
+
b(880, audioCtx.currentTime);
|
|
114
|
+
b(880, audioCtx.currentTime + 0.6);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function playGybeAlarm() {
|
|
118
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
119
|
+
const n = Date.now();
|
|
120
|
+
if (n - lastAlarmTime < 2000) return;
|
|
121
|
+
lastAlarmTime = n;
|
|
122
|
+
|
|
123
|
+
function note(f, s, d) {
|
|
124
|
+
const o = audioCtx.createOscillator();
|
|
125
|
+
const g = audioCtx.createGain();
|
|
126
|
+
o.connect(g);
|
|
127
|
+
g.connect(audioCtx.destination);
|
|
128
|
+
o.type = 'square';
|
|
129
|
+
o.frequency.value = f;
|
|
130
|
+
g.gain.setValueAtTime(0.05, s);
|
|
131
|
+
g.gain.exponentialRampToValueAtTime(0.001, s + d);
|
|
132
|
+
o.start(s);
|
|
133
|
+
o.stop(s + d);
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < 4; i++) {
|
|
136
|
+
note(1800, audioCtx.currentTime + (i * 0.15), 0.1);
|
|
137
|
+
note(1200, audioCtx.currentTime + (i * 0.15) + 0.07, 0.1);
|
|
138
|
+
}
|
|
139
|
+
}
|