@jjlmoya/utils-nature 1.1.0

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.
Files changed (61) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +110 -0
  3. package/src/category/i18n/es.ts +127 -0
  4. package/src/category/i18n/fr.ts +110 -0
  5. package/src/category/index.ts +14 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +30 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/cricketThermometer/bibliography.astro +14 -0
  25. package/src/tool/cricketThermometer/component.astro +549 -0
  26. package/src/tool/cricketThermometer/i18n/en.ts +181 -0
  27. package/src/tool/cricketThermometer/i18n/es.ts +181 -0
  28. package/src/tool/cricketThermometer/i18n/fr.ts +181 -0
  29. package/src/tool/cricketThermometer/index.ts +34 -0
  30. package/src/tool/cricketThermometer/logic.ts +6 -0
  31. package/src/tool/cricketThermometer/seo.astro +15 -0
  32. package/src/tool/cricketThermometer/ui.ts +11 -0
  33. package/src/tool/digitalCarbon/bibliography.astro +9 -0
  34. package/src/tool/digitalCarbon/component.astro +582 -0
  35. package/src/tool/digitalCarbon/i18n/en.ts +235 -0
  36. package/src/tool/digitalCarbon/i18n/es.ts +235 -0
  37. package/src/tool/digitalCarbon/i18n/fr.ts +235 -0
  38. package/src/tool/digitalCarbon/index.ts +33 -0
  39. package/src/tool/digitalCarbon/logic.ts +107 -0
  40. package/src/tool/digitalCarbon/seo.astro +14 -0
  41. package/src/tool/digitalCarbon/ui.ts +38 -0
  42. package/src/tool/rainHarvester/bibliography.astro +9 -0
  43. package/src/tool/rainHarvester/component.astro +559 -0
  44. package/src/tool/rainHarvester/i18n/en.ts +185 -0
  45. package/src/tool/rainHarvester/i18n/es.ts +185 -0
  46. package/src/tool/rainHarvester/i18n/fr.ts +185 -0
  47. package/src/tool/rainHarvester/index.ts +33 -0
  48. package/src/tool/rainHarvester/logic.ts +12 -0
  49. package/src/tool/rainHarvester/seo.astro +14 -0
  50. package/src/tool/rainHarvester/ui.ts +23 -0
  51. package/src/tool/seedCalculator/bibliography.astro +8 -0
  52. package/src/tool/seedCalculator/component.astro +812 -0
  53. package/src/tool/seedCalculator/i18n/en.ts +213 -0
  54. package/src/tool/seedCalculator/i18n/es.ts +213 -0
  55. package/src/tool/seedCalculator/i18n/fr.ts +213 -0
  56. package/src/tool/seedCalculator/index.ts +34 -0
  57. package/src/tool/seedCalculator/logic.ts +19 -0
  58. package/src/tool/seedCalculator/seo.astro +9 -0
  59. package/src/tool/seedCalculator/ui.ts +39 -0
  60. package/src/tools.ts +12 -0
  61. package/src/types.ts +72 -0
@@ -0,0 +1,549 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ interface Props {
4
+ ui: Record<string, string>;
5
+ }
6
+ const { ui } = Astro.props;
7
+ ---
8
+
9
+ <div
10
+ class="ct-card"
11
+ data-label-waiting={ui.labelWaiting}
12
+ data-label-tapping={ui.labelTapping}
13
+ data-sound-on={ui.btnSoundOn}
14
+ data-sound-off={ui.btnSoundOff}
15
+ data-unit-chirps={ui.unitChirpsMin}
16
+ >
17
+ <div class="ct-image-wrapper">
18
+ <img
19
+ src="/images/utilities/cricket-thermometer.webp"
20
+ alt="Cricket on Thermometer Watercolor"
21
+ class="ct-image"
22
+ />
23
+ <div class="ct-image-gradient"></div>
24
+ </div>
25
+
26
+ <div class="ct-body">
27
+ <div class="ct-display-wrapper" id="ct-display-wrapper">
28
+ <div class="ct-glow"></div>
29
+ <div class="ct-circle">
30
+ <div class="ct-inner">
31
+ <div class="ct-temp-row">
32
+ <span id="ct-temp-value" class="ct-temp-value">--</span>
33
+ <span class="ct-temp-unit">°C</span>
34
+ </div>
35
+ <div id="ct-bpm-label" class="ct-bpm-label">{ui.labelWaiting}</div>
36
+ </div>
37
+ <div id="ct-fill" class="ct-fill"></div>
38
+ </div>
39
+ </div>
40
+
41
+ <button id="ct-tap-btn" class="ct-tap-btn" type="button">
42
+ <Icon name="mdi:grass" class="ct-tap-icon" aria-hidden="true" />
43
+ <span class="ct-tap-label">TAP</span>
44
+ <span class="ct-tap-hint">{ui.tapInstruction}</span>
45
+ </button>
46
+
47
+ <div class="ct-controls">
48
+ <button id="ct-reset-btn" class="ct-reset-btn" type="button">
49
+ {ui.btnReset}
50
+ </button>
51
+ <button id="ct-audio-btn" class="ct-audio-btn" type="button">
52
+ <svg
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ width="16"
55
+ height="16"
56
+ fill="none"
57
+ viewBox="0 0 24 24"
58
+ stroke="currentColor"
59
+ aria-hidden="true"
60
+ >
61
+ <path
62
+ stroke-linecap="round"
63
+ stroke-linejoin="round"
64
+ stroke-width="2"
65
+ d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
66
+ ></path>
67
+ </svg>
68
+ <span id="ct-audio-status"></span>
69
+ </button>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <style>
75
+ .ct-card {
76
+ --ct-bg-card: #fff;
77
+ --ct-bg-image: #f0fdf4;
78
+ --ct-gradient-from: #fff;
79
+ --ct-border: #d1fae5;
80
+ --ct-shadow: rgba(0, 0, 0, 0.1);
81
+ --ct-circle-bg: #f0fdf4;
82
+ --ct-circle-border: #a7f3d0;
83
+ --ct-circle-shadow: rgba(0, 0, 0, 0.08);
84
+ --ct-temp-color: #064e3b;
85
+ --ct-unit-color: #059669;
86
+ --ct-bpm-color: #6b7280;
87
+ --ct-btn-border: #d1fae5;
88
+ --ct-btn-color: #374151;
89
+ --ct-btn-hover-bg: #ecfdf5;
90
+ --ct-btn-hover-color: #064e3b;
91
+ --ct-audio-active: #059669;
92
+ --ct-glow: rgba(16, 185, 129, 0.15);
93
+ --ct-tap-hint: rgba(4, 47, 46, 0.65);
94
+ --ct-tap-bg-from: #84cc16;
95
+ --ct-tap-bg-to: #059669;
96
+ --ct-tap-shadow: rgba(5, 150, 105, 0.35);
97
+ --ct-tap-border: rgba(16, 185, 129, 0.3);
98
+ --ct-fill-colors: #3b82f6, #10b981, #ef4444;
99
+
100
+ position: relative;
101
+ width: 100%;
102
+ max-width: 32rem;
103
+ margin: 0 auto;
104
+ background: var(--ct-bg-card);
105
+ border-radius: 2rem;
106
+ overflow: hidden;
107
+ box-shadow: 0 25px 50px -12px var(--ct-shadow);
108
+ border: 1px solid var(--ct-border);
109
+ transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
110
+ }
111
+
112
+ :global(.theme-dark) .ct-card {
113
+ --ct-bg-card: #0f172a;
114
+ --ct-bg-image: #1e293b;
115
+ --ct-gradient-from: #0f172a;
116
+ --ct-border: #1e293b;
117
+ --ct-shadow: rgba(0, 0, 0, 0.5);
118
+ --ct-circle-bg: #1e293b;
119
+ --ct-circle-border: #334155;
120
+ --ct-circle-shadow: rgba(0, 0, 0, 0.3);
121
+ --ct-temp-color: #fff;
122
+ --ct-unit-color: #34d399;
123
+ --ct-bpm-color: #64748b;
124
+ --ct-btn-border: #334155;
125
+ --ct-btn-color: #94a3b8;
126
+ --ct-btn-hover-bg: #1e293b;
127
+ --ct-btn-hover-color: #fff;
128
+ --ct-audio-active: #34d399;
129
+ --ct-glow: rgba(16, 185, 129, 0.2);
130
+ --ct-tap-hint: rgba(209, 250, 229, 0.8);
131
+ --ct-tap-shadow: rgba(6, 78, 59, 0.5);
132
+ }
133
+
134
+ .ct-image-wrapper {
135
+ position: relative;
136
+ height: 16rem;
137
+ width: 100%;
138
+ background: var(--ct-bg-image);
139
+ overflow: hidden;
140
+ }
141
+
142
+ .ct-image {
143
+ display: block;
144
+ object-fit: cover;
145
+ width: 100%;
146
+ height: 100%;
147
+ }
148
+
149
+ .ct-image-gradient {
150
+ position: absolute;
151
+ inset: 0;
152
+ background: linear-gradient(to top, var(--ct-gradient-from), transparent, transparent);
153
+ opacity: 0.9;
154
+ }
155
+
156
+ .ct-body {
157
+ padding: 2rem;
158
+ margin-top: -5rem;
159
+ position: relative;
160
+ z-index: 10;
161
+ display: flex;
162
+ flex-direction: column;
163
+ align-items: center;
164
+ }
165
+
166
+ .ct-display-wrapper {
167
+ position: relative;
168
+ margin-bottom: 2rem;
169
+ }
170
+
171
+ .ct-glow {
172
+ position: absolute;
173
+ inset: -1rem;
174
+ background: var(--ct-glow);
175
+ border-radius: 50%;
176
+ filter: blur(2rem);
177
+ transition: opacity 1s ease;
178
+ opacity: 0;
179
+ pointer-events: none;
180
+ }
181
+
182
+ :global(.ct-display-wrapper.has-value) .ct-glow {
183
+ opacity: 1;
184
+ }
185
+
186
+ .ct-circle {
187
+ width: 12rem;
188
+ height: 12rem;
189
+ background: var(--ct-circle-bg);
190
+ border-radius: 50%;
191
+ border: 4px solid var(--ct-circle-border);
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: center;
195
+ box-shadow: 0 20px 25px -5px var(--ct-circle-shadow);
196
+ overflow: hidden;
197
+ position: relative;
198
+ transition: background 0.3s ease, border-color 0.3s ease;
199
+ }
200
+
201
+ .ct-inner {
202
+ position: relative;
203
+ z-index: 2;
204
+ text-align: center;
205
+ }
206
+
207
+ .ct-temp-row {
208
+ display: flex;
209
+ align-items: baseline;
210
+ justify-content: center;
211
+ gap: 0.15rem;
212
+ }
213
+
214
+ .ct-temp-value {
215
+ font-size: 3rem;
216
+ font-weight: 700;
217
+ color: var(--ct-temp-color);
218
+ transition: all 0.3s ease;
219
+ }
220
+
221
+ .ct-temp-unit {
222
+ font-size: 1.5rem;
223
+ color: var(--ct-unit-color);
224
+ font-weight: 700;
225
+ }
226
+
227
+ .ct-bpm-label {
228
+ font-size: 0.75rem;
229
+ color: var(--ct-bpm-color);
230
+ margin-top: 0.5rem;
231
+ }
232
+
233
+ .ct-fill {
234
+ position: absolute;
235
+ bottom: 0;
236
+ left: 0;
237
+ right: 0;
238
+ background: linear-gradient(to top, var(--ct-fill-colors));
239
+ transition: height 1s ease, opacity 1s ease;
240
+ opacity: 0.2;
241
+ height: 0;
242
+ }
243
+
244
+ .ct-tap-btn {
245
+ width: 100%;
246
+ max-width: 20rem;
247
+ aspect-ratio: 1;
248
+ border-radius: 50%;
249
+ background: linear-gradient(135deg, var(--ct-tap-bg-from), var(--ct-tap-bg-to));
250
+ box-shadow: 0 10px 15px -3px var(--ct-tap-shadow);
251
+ display: flex;
252
+ flex-direction: column;
253
+ align-items: center;
254
+ justify-content: center;
255
+ gap: 0.25rem;
256
+ transform: scale(1);
257
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
258
+ border: 4px solid var(--ct-tap-border);
259
+ margin-bottom: 1.5rem;
260
+ cursor: pointer;
261
+ }
262
+
263
+ .ct-tap-btn:hover {
264
+ transform: scale(1.05);
265
+ box-shadow: 0 10px 20px var(--ct-glow);
266
+ }
267
+
268
+ .ct-tap-btn:active {
269
+ transform: scale(0.95);
270
+ }
271
+
272
+ :global(.ct-tap-btn.ct-tap-pressed) {
273
+ transform: scale(0.95);
274
+ }
275
+
276
+ .ct-tap-icon {
277
+ font-size: 2.5rem;
278
+ color: #fff;
279
+ pointer-events: none;
280
+ }
281
+
282
+ .ct-tap-label {
283
+ font-size: 1.25rem;
284
+ font-weight: 700;
285
+ color: #fff;
286
+ text-transform: uppercase;
287
+ letter-spacing: 0.2em;
288
+ pointer-events: none;
289
+ }
290
+
291
+ .ct-tap-hint {
292
+ font-size: 0.75rem;
293
+ color: var(--ct-tap-hint);
294
+ pointer-events: none;
295
+ }
296
+
297
+ .ct-controls {
298
+ display: flex;
299
+ gap: 1rem;
300
+ }
301
+
302
+ .ct-reset-btn,
303
+ .ct-audio-btn {
304
+ padding: 0.5rem 1.5rem;
305
+ border-radius: 9999px;
306
+ border: 1px solid var(--ct-btn-border);
307
+ color: var(--ct-btn-color);
308
+ background: transparent;
309
+ cursor: pointer;
310
+ font-size: 0.875rem;
311
+ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
312
+ }
313
+
314
+ .ct-reset-btn:hover,
315
+ .ct-audio-btn:hover {
316
+ background: var(--ct-btn-hover-bg);
317
+ color: var(--ct-btn-hover-color);
318
+ }
319
+
320
+ .ct-audio-btn {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 0.5rem;
324
+ }
325
+
326
+ :global(.ct-audio-btn.audio-active) {
327
+ color: var(--ct-audio-active);
328
+ }
329
+ </style>
330
+
331
+ <script>
332
+ const card = document.querySelector<HTMLElement>(".ct-card");
333
+
334
+ const LABELS = {
335
+ waiting: card?.dataset.labelWaiting ?? "...",
336
+ tapping: card?.dataset.labelTapping ?? "...",
337
+ soundOn: card?.dataset.soundOn ?? "On",
338
+ soundOff: card?.dataset.soundOff ?? "Off",
339
+ unitChirps: card?.dataset.unitChirps ?? "chirps/min",
340
+ };
341
+
342
+ class CricketThermometer {
343
+ private tapBtn = document.getElementById("ct-tap-btn") as HTMLButtonElement;
344
+ private resetBtn = document.getElementById("ct-reset-btn") as HTMLButtonElement;
345
+ private audioBtn = document.getElementById("ct-audio-btn") as HTMLButtonElement;
346
+ private audioStatus = document.getElementById("ct-audio-status") as HTMLElement;
347
+ private tempValue = document.getElementById("ct-temp-value") as HTMLElement;
348
+ private bpmLabel = document.getElementById("ct-bpm-label") as HTMLElement;
349
+ private fill = document.getElementById("ct-fill") as HTMLElement;
350
+ private displayWrapper = document.getElementById("ct-display-wrapper") as HTMLElement;
351
+
352
+ private taps: number[] = [];
353
+ private maxTapsToKeep = 5;
354
+ private lastTapTime = 0;
355
+
356
+ private ctx: AudioContext | null = null;
357
+ private isAudioEnabled = false;
358
+ private isPlayingLoop = false;
359
+ private loopInterval: number | undefined;
360
+ private currentBPM = 0;
361
+
362
+ constructor() {
363
+ this.audioStatus.textContent = LABELS.soundOff;
364
+ this.initEvents();
365
+ }
366
+
367
+ private initEvents() {
368
+ this.tapBtn.addEventListener("touchstart", (e) => {
369
+ e.preventDefault();
370
+ this.handleTap();
371
+ });
372
+ this.tapBtn.addEventListener("mousedown", () => this.handleTap());
373
+
374
+ document.addEventListener("keydown", (e) => {
375
+ if (e.code === "Space") {
376
+ e.preventDefault();
377
+ this.tapBtn.classList.add("ct-tap-pressed");
378
+ this.handleTap();
379
+ }
380
+ });
381
+ document.addEventListener("keyup", (e) => {
382
+ if (e.code === "Space") {
383
+ this.tapBtn.classList.remove("ct-tap-pressed");
384
+ }
385
+ });
386
+
387
+ this.resetBtn.addEventListener("click", () => this.reset());
388
+
389
+ this.audioBtn.addEventListener("click", () => {
390
+ this.isAudioEnabled = !this.isAudioEnabled;
391
+ this.updateAudioUI();
392
+ if (this.isAudioEnabled) {
393
+ this.initAudioContext();
394
+ this.updateChirpLoop();
395
+ } else {
396
+ this.stopChirpLoop();
397
+ }
398
+ });
399
+ }
400
+
401
+ private handleTap() {
402
+ const now = Date.now();
403
+ if (!this.ctx) this.initAudioContext();
404
+ else if (this.ctx.state === "suspended") this.ctx.resume();
405
+
406
+ this.playClickSound();
407
+
408
+ if (now - this.lastTapTime > 3000 && this.lastTapTime !== 0) {
409
+ this.taps = [];
410
+ }
411
+ this.lastTapTime = now;
412
+ this.taps.push(now);
413
+ if (this.taps.length > this.maxTapsToKeep) this.taps.shift();
414
+
415
+ this.calculate();
416
+ }
417
+
418
+ private calculate() {
419
+ if (this.taps.length < 2) {
420
+ this.bpmLabel.textContent = LABELS.tapping;
421
+ return;
422
+ }
423
+
424
+ let totalInterval = 0;
425
+ for (let i = 1; i < this.taps.length; i++) {
426
+ totalInterval += this.taps[i] - this.taps[i - 1];
427
+ }
428
+ const avgInterval = totalInterval / (this.taps.length - 1);
429
+ const bpm = 60000 / avgInterval;
430
+ this.currentBPM = bpm;
431
+
432
+ const temperature = 10 + (bpm - 40) / 7;
433
+ this.updateUI(temperature, bpm);
434
+ this.updateChirpLoop();
435
+ }
436
+
437
+ private updateUI(temp: number, bpm: number) {
438
+ const displayTemp = temp < 0 ? 0 : temp;
439
+ this.tempValue.textContent = displayTemp.toFixed(1);
440
+ this.bpmLabel.textContent = `${Math.round(bpm)} ${LABELS.unitChirps}`;
441
+ this.displayWrapper.classList.add("has-value");
442
+ const percent = Math.min(100, Math.max(0, (displayTemp / 45) * 100));
443
+ this.fill.style.height = `${percent}%`;
444
+ this.fill.style.opacity = "0.4";
445
+ }
446
+
447
+ private reset() {
448
+ this.taps = [];
449
+ this.tempValue.textContent = "--";
450
+ this.bpmLabel.textContent = LABELS.waiting;
451
+ this.fill.style.height = "0";
452
+ this.displayWrapper.classList.remove("has-value");
453
+ this.stopChirpLoop();
454
+ this.currentBPM = 0;
455
+ }
456
+
457
+ private initAudioContext() {
458
+ if (!this.ctx) {
459
+ this.ctx = new (window.AudioContext ||
460
+ (window as unknown as { webkitAudioContext: typeof AudioContext })
461
+ .webkitAudioContext)();
462
+ }
463
+ }
464
+
465
+ private playClickSound() {
466
+ if (!this.ctx) return;
467
+ const osc = this.ctx.createOscillator();
468
+ const gain = this.ctx.createGain();
469
+ osc.frequency.value = 800;
470
+ osc.type = "triangle";
471
+ gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
472
+ gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.1);
473
+ osc.connect(gain);
474
+ gain.connect(this.ctx.destination);
475
+ osc.start();
476
+ osc.stop(this.ctx.currentTime + 0.1);
477
+ }
478
+
479
+ private updateAudioUI() {
480
+ if (this.isAudioEnabled) {
481
+ this.audioStatus.textContent = LABELS.soundOn;
482
+ this.audioBtn.classList.add("audio-active");
483
+ } else {
484
+ this.audioStatus.textContent = LABELS.soundOff;
485
+ this.audioBtn.classList.remove("audio-active");
486
+ }
487
+ }
488
+
489
+ private stopChirpLoop() {
490
+ this.isPlayingLoop = false;
491
+ if (this.loopInterval !== undefined) {
492
+ window.clearTimeout(this.loopInterval);
493
+ this.loopInterval = undefined;
494
+ }
495
+ }
496
+
497
+ private updateChirpLoop() {
498
+ if (!this.isAudioEnabled || this.currentBPM <= 0) return;
499
+ if (!this.isPlayingLoop) {
500
+ this.isPlayingLoop = true;
501
+ this.scheduleNextChirp();
502
+ }
503
+ }
504
+
505
+ private scheduleNextChirp() {
506
+ if (!this.isPlayingLoop || !this.isAudioEnabled || this.currentBPM <= 0) {
507
+ this.isPlayingLoop = false;
508
+ return;
509
+ }
510
+ this.playCricketChirp();
511
+ const intervalMs = 60000 / this.currentBPM;
512
+ this.loopInterval = window.setTimeout(
513
+ () => this.scheduleNextChirp(),
514
+ intervalMs
515
+ );
516
+ }
517
+
518
+ private playCricketChirp() {
519
+ if (!this.ctx) return;
520
+ const t = this.ctx.currentTime;
521
+
522
+ const osc = this.ctx.createOscillator();
523
+ const osc2 = this.ctx.createOscillator();
524
+ const gain = this.ctx.createGain();
525
+
526
+ osc.frequency.setValueAtTime(4500, t);
527
+ osc.type = "sine";
528
+ osc2.frequency.setValueAtTime(4650, t);
529
+ osc2.type = "sine";
530
+
531
+ gain.gain.setValueAtTime(0, t);
532
+ gain.gain.linearRampToValueAtTime(0.1, t + 0.01);
533
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
534
+
535
+ osc.connect(gain);
536
+ osc2.connect(gain);
537
+ gain.connect(this.ctx.destination);
538
+
539
+ osc.start(t);
540
+ osc.stop(t + 0.2);
541
+ osc2.start(t);
542
+ osc2.stop(t + 0.2);
543
+ }
544
+ }
545
+
546
+ document.addEventListener("DOMContentLoaded", () => {
547
+ new CricketThermometer();
548
+ });
549
+ </script>