@jjlmoya/utils-health 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 (155) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +60 -0
  3. package/src/category/i18n/es.ts +60 -0
  4. package/src/category/i18n/fr.ts +60 -0
  5. package/src/category/index.ts +22 -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 +28 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +36 -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/binauralTuner/bibliography.astro +14 -0
  25. package/src/tool/binauralTuner/component.astro +687 -0
  26. package/src/tool/binauralTuner/i18n/en.ts +187 -0
  27. package/src/tool/binauralTuner/i18n/es.ts +187 -0
  28. package/src/tool/binauralTuner/i18n/fr.ts +187 -0
  29. package/src/tool/binauralTuner/index.ts +27 -0
  30. package/src/tool/binauralTuner/seo.astro +14 -0
  31. package/src/tool/binauralTuner/ui.ts +18 -0
  32. package/src/tool/bloodUnitConverter/bibliography.astro +14 -0
  33. package/src/tool/bloodUnitConverter/component.astro +915 -0
  34. package/src/tool/bloodUnitConverter/i18n/en.ts +227 -0
  35. package/src/tool/bloodUnitConverter/i18n/es.ts +250 -0
  36. package/src/tool/bloodUnitConverter/i18n/fr.ts +218 -0
  37. package/src/tool/bloodUnitConverter/index.ts +27 -0
  38. package/src/tool/bloodUnitConverter/seo.astro +14 -0
  39. package/src/tool/bloodUnitConverter/ui.ts +38 -0
  40. package/src/tool/bmiCalculator/bibliography.astro +14 -0
  41. package/src/tool/bmiCalculator/component.astro +415 -0
  42. package/src/tool/bmiCalculator/i18n/en.ts +217 -0
  43. package/src/tool/bmiCalculator/i18n/es.ts +221 -0
  44. package/src/tool/bmiCalculator/i18n/fr.ts +217 -0
  45. package/src/tool/bmiCalculator/index.ts +27 -0
  46. package/src/tool/bmiCalculator/seo.astro +14 -0
  47. package/src/tool/bmiCalculator/ui.ts +21 -0
  48. package/src/tool/breathingVisualizer/bibliography.astro +14 -0
  49. package/src/tool/breathingVisualizer/component.astro +636 -0
  50. package/src/tool/breathingVisualizer/i18n/en.ts +206 -0
  51. package/src/tool/breathingVisualizer/i18n/es.ts +206 -0
  52. package/src/tool/breathingVisualizer/i18n/fr.ts +206 -0
  53. package/src/tool/breathingVisualizer/index.ts +27 -0
  54. package/src/tool/breathingVisualizer/seo.astro +14 -0
  55. package/src/tool/breathingVisualizer/ui.ts +31 -0
  56. package/src/tool/caffeineTracker/bibliography.astro +14 -0
  57. package/src/tool/caffeineTracker/component.astro +1210 -0
  58. package/src/tool/caffeineTracker/i18n/en.ts +198 -0
  59. package/src/tool/caffeineTracker/i18n/es.ts +198 -0
  60. package/src/tool/caffeineTracker/i18n/fr.ts +198 -0
  61. package/src/tool/caffeineTracker/index.ts +27 -0
  62. package/src/tool/caffeineTracker/logic.ts +31 -0
  63. package/src/tool/caffeineTracker/seo.astro +14 -0
  64. package/src/tool/caffeineTracker/ui.ts +36 -0
  65. package/src/tool/daltonismSimulator/bibliography.astro +14 -0
  66. package/src/tool/daltonismSimulator/component.astro +383 -0
  67. package/src/tool/daltonismSimulator/i18n/en.ts +188 -0
  68. package/src/tool/daltonismSimulator/i18n/es.ts +218 -0
  69. package/src/tool/daltonismSimulator/i18n/fr.ts +168 -0
  70. package/src/tool/daltonismSimulator/index.ts +27 -0
  71. package/src/tool/daltonismSimulator/seo.astro +14 -0
  72. package/src/tool/daltonismSimulator/ui.ts +20 -0
  73. package/src/tool/digestionStopwatch/bibliography.astro +14 -0
  74. package/src/tool/digestionStopwatch/component.astro +627 -0
  75. package/src/tool/digestionStopwatch/i18n/en.ts +173 -0
  76. package/src/tool/digestionStopwatch/i18n/es.ts +173 -0
  77. package/src/tool/digestionStopwatch/i18n/fr.ts +173 -0
  78. package/src/tool/digestionStopwatch/index.ts +27 -0
  79. package/src/tool/digestionStopwatch/logic.ts +63 -0
  80. package/src/tool/digestionStopwatch/seo.astro +14 -0
  81. package/src/tool/digestionStopwatch/ui.ts +20 -0
  82. package/src/tool/epworthSleepinessScale/bibliography.astro +14 -0
  83. package/src/tool/epworthSleepinessScale/component.astro +528 -0
  84. package/src/tool/epworthSleepinessScale/i18n/en.ts +217 -0
  85. package/src/tool/epworthSleepinessScale/i18n/es.ts +217 -0
  86. package/src/tool/epworthSleepinessScale/i18n/fr.ts +217 -0
  87. package/src/tool/epworthSleepinessScale/index.ts +27 -0
  88. package/src/tool/epworthSleepinessScale/seo.astro +14 -0
  89. package/src/tool/epworthSleepinessScale/ui.ts +27 -0
  90. package/src/tool/hydrationCalculator/bibliography.astro +14 -0
  91. package/src/tool/hydrationCalculator/component.astro +694 -0
  92. package/src/tool/hydrationCalculator/i18n/en.ts +217 -0
  93. package/src/tool/hydrationCalculator/i18n/es.ts +222 -0
  94. package/src/tool/hydrationCalculator/i18n/fr.ts +199 -0
  95. package/src/tool/hydrationCalculator/index.ts +27 -0
  96. package/src/tool/hydrationCalculator/seo.astro +14 -0
  97. package/src/tool/hydrationCalculator/ui.ts +28 -0
  98. package/src/tool/pelliRobsonTest/bibliography.astro +14 -0
  99. package/src/tool/pelliRobsonTest/component.astro +653 -0
  100. package/src/tool/pelliRobsonTest/i18n/en.ts +205 -0
  101. package/src/tool/pelliRobsonTest/i18n/es.ts +205 -0
  102. package/src/tool/pelliRobsonTest/i18n/fr.ts +205 -0
  103. package/src/tool/pelliRobsonTest/index.ts +27 -0
  104. package/src/tool/pelliRobsonTest/seo.astro +14 -0
  105. package/src/tool/pelliRobsonTest/ui.ts +21 -0
  106. package/src/tool/peripheralVisionTrainer/bibliography.astro +14 -0
  107. package/src/tool/peripheralVisionTrainer/component.astro +678 -0
  108. package/src/tool/peripheralVisionTrainer/i18n/en.ts +224 -0
  109. package/src/tool/peripheralVisionTrainer/i18n/es.ts +224 -0
  110. package/src/tool/peripheralVisionTrainer/i18n/fr.ts +211 -0
  111. package/src/tool/peripheralVisionTrainer/index.ts +27 -0
  112. package/src/tool/peripheralVisionTrainer/seo.astro +14 -0
  113. package/src/tool/peripheralVisionTrainer/ui.ts +26 -0
  114. package/src/tool/readingDistanceCalculator/bibliography.astro +14 -0
  115. package/src/tool/readingDistanceCalculator/component.astro +588 -0
  116. package/src/tool/readingDistanceCalculator/i18n/en.ts +202 -0
  117. package/src/tool/readingDistanceCalculator/i18n/es.ts +215 -0
  118. package/src/tool/readingDistanceCalculator/i18n/fr.ts +193 -0
  119. package/src/tool/readingDistanceCalculator/index.ts +31 -0
  120. package/src/tool/readingDistanceCalculator/seo.astro +14 -0
  121. package/src/tool/readingDistanceCalculator/ui.ts +18 -0
  122. package/src/tool/screenDecompressionTime/bibliography.astro +14 -0
  123. package/src/tool/screenDecompressionTime/component.astro +671 -0
  124. package/src/tool/screenDecompressionTime/i18n/en.ts +225 -0
  125. package/src/tool/screenDecompressionTime/i18n/es.ts +247 -0
  126. package/src/tool/screenDecompressionTime/i18n/fr.ts +225 -0
  127. package/src/tool/screenDecompressionTime/index.ts +27 -0
  128. package/src/tool/screenDecompressionTime/seo.astro +14 -0
  129. package/src/tool/screenDecompressionTime/ui.ts +32 -0
  130. package/src/tool/tinnitusReliever/bibliography.astro +14 -0
  131. package/src/tool/tinnitusReliever/component.astro +581 -0
  132. package/src/tool/tinnitusReliever/i18n/en.ts +161 -0
  133. package/src/tool/tinnitusReliever/i18n/es.ts +161 -0
  134. package/src/tool/tinnitusReliever/i18n/fr.ts +161 -0
  135. package/src/tool/tinnitusReliever/index.ts +27 -0
  136. package/src/tool/tinnitusReliever/seo.astro +14 -0
  137. package/src/tool/tinnitusReliever/ui.ts +9 -0
  138. package/src/tool/ubeCalculator/bibliography.astro +14 -0
  139. package/src/tool/ubeCalculator/component.astro +683 -0
  140. package/src/tool/ubeCalculator/i18n/en.ts +200 -0
  141. package/src/tool/ubeCalculator/i18n/es.ts +200 -0
  142. package/src/tool/ubeCalculator/i18n/fr.ts +196 -0
  143. package/src/tool/ubeCalculator/index.ts +27 -0
  144. package/src/tool/ubeCalculator/seo.astro +14 -0
  145. package/src/tool/ubeCalculator/ui.ts +26 -0
  146. package/src/tool/waterPurifier/bibliography.astro +14 -0
  147. package/src/tool/waterPurifier/component.astro +628 -0
  148. package/src/tool/waterPurifier/i18n/en.ts +167 -0
  149. package/src/tool/waterPurifier/i18n/es.ts +167 -0
  150. package/src/tool/waterPurifier/i18n/fr.ts +167 -0
  151. package/src/tool/waterPurifier/index.ts +27 -0
  152. package/src/tool/waterPurifier/seo.astro +14 -0
  153. package/src/tool/waterPurifier/ui.ts +18 -0
  154. package/src/tools.ts +19 -0
  155. package/src/types.ts +72 -0
@@ -0,0 +1,581 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { KnownLocale } from '../../types';
4
+ import type { TinnitusRelieverUI } from './ui';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ ui?: Record<string, unknown>;
9
+ }
10
+
11
+ const t = (Astro.props.ui ?? {}) as TinnitusRelieverUI;
12
+ ---
13
+
14
+ <div class="tr">
15
+ <div class="tr__visualizer">
16
+ <canvas id="noise-visualizer" class="tr__canvas"></canvas>
17
+ <div class="tr__canvas-overlay"></div>
18
+ <button id="play-btn" class="tr__play-btn">
19
+ <div class="tr__play-glow"></div>
20
+ <div class="tr__play-inner">
21
+ <span id="play-icon" class="tr__icon tr__icon--play">
22
+ <Icon name="mdi:play" />
23
+ </span>
24
+ <span id="stop-icon" class="tr__icon tr__icon--stop tr__icon--hidden">
25
+ <Icon name="mdi:stop" />
26
+ </span>
27
+ </div>
28
+ </button>
29
+ </div>
30
+
31
+ <div class="tr__controls">
32
+ <div class="tr__noise-btns">
33
+ <button class="tr__noise-btn" data-type="white" data-active="true">{t.typeWhite ?? 'White Noise'}</button>
34
+ <button class="tr__noise-btn" data-type="pink" data-active="false">{t.typePink ?? 'Pink Noise'}</button>
35
+ <button class="tr__noise-btn" data-type="brown" data-active="false">{t.typeBrown ?? 'Brown Noise'}</button>
36
+ </div>
37
+
38
+ <div class="tr__sliders">
39
+ <div class="tr__slider-group">
40
+ <div class="tr__slider-header">
41
+ <label class="tr__label" for="freq-slider">{t.labelFrequency ?? 'Tinnitus Frequency'}</label>
42
+ <span class="tr__value"><span id="freq-display">4000</span> Hz</span>
43
+ </div>
44
+ <input type="range" min="200" max="12000" step="50" value="4000" id="freq-slider" class="tr__slider">
45
+ <p class="tr__hint">{t.hintFrequency ?? 'Move this slider until it matches the tone of your tinnitus.'}</p>
46
+ </div>
47
+
48
+ <div class="tr__slider-group">
49
+ <div class="tr__slider-header">
50
+ <label class="tr__label" for="vol-slider">{t.labelVolume ?? 'Volume'}</label>
51
+ <span class="tr__value"><span id="vol-display">50</span>%</span>
52
+ </div>
53
+ <input type="range" min="0" max="100" value="50" id="vol-slider" class="tr__slider">
54
+ <p class="tr__hint">{t.hintVolume ?? 'Adjust the volume to a comfortable level, just below your tinnitus.'}</p>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <style>
61
+ .tr {
62
+ --tr-accent: #0d9488;
63
+ --tr-bg-canvas: #f1f5f9;
64
+ --tr-bg-controls: #fff;
65
+ --tr-text: #1e293b;
66
+ --tr-label: #64748b;
67
+ --tr-border: #e2e8f0;
68
+ --tr-track: #e2e8f0;
69
+ --tr-btn-hover: #f8fafc;
70
+ --tr-play-inner: rgba(255, 255, 255, 0.9);
71
+ --tr-overlay-start: rgba(248, 250, 252, 0.1);
72
+ --tr-thumb-bg: #fff;
73
+
74
+ position: relative;
75
+ width: 100%;
76
+ max-width: 56rem;
77
+ margin: 0 auto;
78
+ background: var(--tr-bg-controls);
79
+ border-radius: 1.5rem;
80
+ overflow: hidden;
81
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
82
+ border: 1px solid var(--tr-border);
83
+ }
84
+
85
+ .tr__visualizer {
86
+ position: relative;
87
+ height: 16rem;
88
+ background: var(--tr-bg-canvas);
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ overflow: hidden;
93
+ }
94
+
95
+ .tr__canvas {
96
+ position: absolute;
97
+ inset: 0;
98
+ width: 100%;
99
+ height: 100%;
100
+ opacity: 0.6;
101
+ }
102
+
103
+ :global(.theme-dark) .tr {
104
+ --tr-accent: #2dd4bf;
105
+ --tr-bg-canvas: #020617;
106
+ --tr-bg-controls: #0f172a;
107
+ --tr-text: #cbd5e1;
108
+ --tr-label: #94a3b8;
109
+ --tr-border: #334155;
110
+ --tr-track: #1e293b;
111
+ --tr-btn-hover: #1e293b;
112
+ --tr-play-inner: rgba(30, 41, 59, 0.8);
113
+ --tr-overlay-start: rgba(15, 23, 42, 0.1);
114
+ --tr-thumb-bg: #0f172a;
115
+ }
116
+
117
+ .tr__canvas-overlay {
118
+ position: absolute;
119
+ inset: 0;
120
+ background: linear-gradient(to bottom, var(--tr-overlay-start), transparent, var(--tr-bg-controls));
121
+ pointer-events: none;
122
+ }
123
+
124
+ .tr__play-btn {
125
+ position: relative;
126
+ z-index: 10;
127
+ background: none;
128
+ border: none;
129
+ cursor: pointer;
130
+ padding: 0;
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ transition: transform 0.3s;
135
+ }
136
+
137
+ .tr__play-btn:hover {
138
+ transform: scale(1.05);
139
+ }
140
+
141
+ .tr__play-glow {
142
+ position: absolute;
143
+ inset: 0;
144
+ background: rgba(20, 184, 166, 0.2);
145
+ filter: blur(2rem);
146
+ border-radius: 50%;
147
+ opacity: 0.2;
148
+ transition: opacity 0.3s;
149
+ pointer-events: none;
150
+ }
151
+
152
+ .tr__play-btn:hover .tr__play-glow {
153
+ opacity: 0.4;
154
+ }
155
+
156
+ .tr__play-inner {
157
+ width: 5rem;
158
+ height: 5rem;
159
+ background: var(--tr-play-inner);
160
+ -webkit-backdrop-filter: blur(12px);
161
+ backdrop-filter: blur(12px);
162
+ border: 1px solid var(--tr-border);
163
+ border-radius: 50%;
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
168
+ transition: border-color 0.3s;
169
+ }
170
+
171
+ .tr__play-btn:hover .tr__play-inner {
172
+ border-color: rgba(45, 212, 191, 0.5);
173
+ }
174
+
175
+ .tr__icon {
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ width: 2rem;
180
+ height: 2rem;
181
+ }
182
+
183
+ .tr__icon svg {
184
+ width: 2rem;
185
+ height: 2rem;
186
+ }
187
+
188
+ .tr__icon--play {
189
+ color: var(--tr-accent);
190
+ }
191
+
192
+ .tr__icon--stop {
193
+ color: #f87171;
194
+ }
195
+
196
+ .tr__icon--hidden {
197
+ display: none;
198
+ }
199
+
200
+ .tr__controls {
201
+ padding: 2rem;
202
+ color: var(--tr-text);
203
+ }
204
+
205
+ .tr__noise-btns {
206
+ display: flex;
207
+ justify-content: center;
208
+ gap: 1rem;
209
+ margin-bottom: 2.5rem;
210
+ flex-wrap: wrap;
211
+ }
212
+
213
+ .tr__noise-btn {
214
+ padding: 0.5rem 1.5rem;
215
+ border-radius: 9999px;
216
+ font-size: 0.875rem;
217
+ font-weight: 500;
218
+ border: 1px solid var(--tr-border);
219
+ background: transparent;
220
+ color: var(--tr-label);
221
+ cursor: pointer;
222
+ transition: all 0.3s;
223
+ }
224
+
225
+ .tr__noise-btn:hover {
226
+ background: var(--tr-btn-hover);
227
+ }
228
+
229
+ .tr__noise-btn[data-active="true"] {
230
+ background: rgba(20, 184, 166, 0.2);
231
+ border-color: rgba(45, 212, 191, 0.5);
232
+ color: var(--tr-accent);
233
+ }
234
+
235
+ .tr__sliders {
236
+ display: grid;
237
+ grid-template-columns: 1fr 1fr;
238
+ gap: 3rem;
239
+ }
240
+
241
+ .tr__slider-group {
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: 1rem;
245
+ }
246
+
247
+ .tr__slider-header {
248
+ display: flex;
249
+ justify-content: space-between;
250
+ align-items: center;
251
+ }
252
+
253
+ .tr__label {
254
+ font-size: 0.75rem;
255
+ font-weight: 500;
256
+ color: var(--tr-label);
257
+ text-transform: uppercase;
258
+ letter-spacing: 0.05em;
259
+ }
260
+
261
+ .tr__value {
262
+ color: var(--tr-accent);
263
+ font-size: 1.125rem;
264
+ }
265
+
266
+ .tr__slider {
267
+ width: 100%;
268
+ appearance: none;
269
+ background: var(--tr-track);
270
+ height: 0.5rem;
271
+ border-radius: 0.25rem;
272
+ cursor: pointer;
273
+ outline: none;
274
+ border: none;
275
+ }
276
+
277
+ .tr__slider::-webkit-slider-thumb {
278
+ -webkit-appearance: none;
279
+ appearance: none;
280
+ width: 1.5rem;
281
+ height: 1.5rem;
282
+ border-radius: 50%;
283
+ background: var(--tr-thumb-bg);
284
+ border: 2px solid var(--tr-accent);
285
+ cursor: pointer;
286
+ box-shadow: 0 0 10px rgba(45, 212, 191, 0.3);
287
+ transition: transform 0.1s;
288
+ }
289
+
290
+ .tr__slider::-webkit-slider-thumb:hover {
291
+ transform: scale(1.1);
292
+ box-shadow: 0 0 15px rgba(45, 212, 191, 0.5);
293
+ }
294
+
295
+ .tr__slider::-moz-range-thumb {
296
+ width: 1.5rem;
297
+ height: 1.5rem;
298
+ border-radius: 50%;
299
+ background: var(--tr-thumb-bg);
300
+ border: 2px solid var(--tr-accent);
301
+ cursor: pointer;
302
+ box-shadow: 0 0 10px rgba(45, 212, 191, 0.3);
303
+ }
304
+
305
+ .tr__hint {
306
+ font-size: 0.75rem;
307
+ color: var(--tr-label);
308
+ margin: 0;
309
+ }
310
+
311
+ @media (max-width: 640px) {
312
+ .tr__sliders {
313
+ grid-template-columns: 1fr;
314
+ gap: 2rem;
315
+ }
316
+
317
+ .tr__noise-btns {
318
+ gap: 0.5rem;
319
+ }
320
+
321
+ .tr__controls {
322
+ padding: 1.5rem;
323
+ }
324
+ }
325
+ </style>
326
+
327
+ <script>
328
+ class TinnitusReliever {
329
+ silentAudio: HTMLAudioElement | null = null;
330
+ hasUnlocked = false;
331
+ ctx: AudioContext | null = null;
332
+ noiseNode: AudioBufferSourceNode | null = null;
333
+ gainNode: GainNode | null = null;
334
+ filterNode: BiquadFilterNode | null = null;
335
+ analyser: AnalyserNode | null = null;
336
+ isPlaying = false;
337
+
338
+ frequency = 4000;
339
+ volume = 0.3;
340
+ noiseType = 'white';
341
+
342
+ playBtn: HTMLButtonElement;
343
+ playIcon: HTMLElement;
344
+ stopIcon: HTMLElement;
345
+ freqSlider: HTMLInputElement;
346
+ volSlider: HTMLInputElement;
347
+ freqDisplay: HTMLElement;
348
+ volDisplay: HTMLElement;
349
+ noiseBtns: NodeListOf<HTMLButtonElement>;
350
+ canvas: HTMLCanvasElement;
351
+ canvasCtx: CanvasRenderingContext2D;
352
+ animationId: number = 0;
353
+
354
+ constructor() {
355
+ this.playBtn = document.querySelector('#play-btn') as HTMLButtonElement;
356
+ this.playIcon = document.querySelector('#play-icon') as HTMLElement;
357
+ this.stopIcon = document.querySelector('#stop-icon') as HTMLElement;
358
+ this.freqSlider = document.querySelector('#freq-slider') as HTMLInputElement;
359
+ this.volSlider = document.querySelector('#vol-slider') as HTMLInputElement;
360
+ this.freqDisplay = document.querySelector('#freq-display') as HTMLElement;
361
+ this.volDisplay = document.querySelector('#vol-display') as HTMLElement;
362
+ this.noiseBtns = document.querySelectorAll('.tr__noise-btn');
363
+ this.canvas = document.querySelector('#noise-visualizer') as HTMLCanvasElement;
364
+ this.canvasCtx = this.canvas.getContext('2d')!;
365
+
366
+ this.initEvents();
367
+ this.resizeCanvas();
368
+ window.addEventListener('resize', () => this.resizeCanvas());
369
+
370
+ this.silentAudio = new Audio(
371
+ 'data:audio/wav;base64,UklGRjIAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YRAAAAAAAAAAAAAAAAAAAA=='
372
+ );
373
+ this.silentAudio.loop = false;
374
+ }
375
+
376
+ initEvents() {
377
+ this.playBtn.addEventListener('click', () => this.togglePlay());
378
+
379
+ this.freqSlider.addEventListener('input', (e) => {
380
+ this.frequency = parseInt((e.target as HTMLInputElement).value);
381
+ this.freqDisplay.textContent = this.frequency.toString();
382
+ this.updateAudioParams();
383
+ });
384
+
385
+ this.volSlider.addEventListener('input', (e) => {
386
+ this.volume = parseInt((e.target as HTMLInputElement).value) / 100;
387
+ this.volDisplay.textContent = (e.target as HTMLInputElement).value;
388
+ this.updateAudioParams();
389
+ });
390
+
391
+ this.noiseBtns.forEach((btn) => {
392
+ btn.addEventListener('click', () => {
393
+ const type = btn.dataset.type!;
394
+ if (this.noiseType === type) return;
395
+ this.noiseType = type;
396
+ this.noiseBtns.forEach((b) => (b.dataset.active = 'false'));
397
+ btn.dataset.active = 'true';
398
+ if (this.isPlaying) {
399
+ this.stopAudio();
400
+ this.startAudio();
401
+ }
402
+ });
403
+ });
404
+ }
405
+
406
+ playSilentUnlock() {
407
+ if (this.silentAudio) {
408
+ this.silentAudio.volume = 0.01;
409
+ this.silentAudio.play().catch((e) => console.error('Silent audio play failed', e));
410
+ }
411
+ }
412
+
413
+ initAudioContext() {
414
+ if (!this.ctx) {
415
+ type ExtWindow = Window & { webkitAudioContext?: typeof AudioContext };
416
+ const Ctor = window.AudioContext ?? (window as unknown as ExtWindow).webkitAudioContext;
417
+ if (Ctor) this.ctx = new Ctor();
418
+ }
419
+ if (this.ctx?.state === 'suspended') {
420
+ this.ctx.resume();
421
+ }
422
+ }
423
+
424
+ async startAudio() {
425
+ this.initAudioContext();
426
+ if (!this.ctx) return;
427
+
428
+ this.gainNode = this.ctx.createGain();
429
+ this.filterNode = this.ctx.createBiquadFilter();
430
+ this.analyser = this.ctx.createAnalyser();
431
+
432
+ this.filterNode.type = 'notch';
433
+ this.filterNode.frequency.value = this.frequency;
434
+ this.filterNode.Q.value = 1.5;
435
+
436
+ this.gainNode.gain.setValueAtTime(0, this.ctx.currentTime);
437
+ this.gainNode.gain.linearRampToValueAtTime(this.volume, this.ctx.currentTime + 1.0);
438
+
439
+ const bufferSize = 2 * this.ctx.sampleRate;
440
+ const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
441
+ const data = buffer.getChannelData(0);
442
+ this.generateNoise(data, this.noiseType);
443
+
444
+ this.noiseNode = this.ctx.createBufferSource();
445
+ this.noiseNode.buffer = buffer;
446
+ this.noiseNode.loop = true;
447
+
448
+ this.noiseNode.connect(this.filterNode);
449
+ this.filterNode.connect(this.gainNode);
450
+ this.gainNode.connect(this.analyser);
451
+ this.analyser.connect(this.ctx.destination);
452
+
453
+ this.noiseNode.start();
454
+ this.isPlaying = true;
455
+ this.updateUIActionState();
456
+ this.draw();
457
+ }
458
+
459
+ stopAudio() {
460
+ if (this.noiseNode) {
461
+ this.noiseNode.stop();
462
+ this.noiseNode.disconnect();
463
+ }
464
+ this.isPlaying = false;
465
+ this.updateUIActionState();
466
+ cancelAnimationFrame(this.animationId);
467
+ this.clearCanvas();
468
+ }
469
+
470
+ togglePlay() {
471
+ this.initAudioContext();
472
+
473
+ if (!this.hasUnlocked) {
474
+ this.playSilentUnlock();
475
+ this.hasUnlocked = true;
476
+ }
477
+
478
+ if (this.isPlaying) {
479
+ this.stopAudio();
480
+ } else {
481
+ this.startAudio();
482
+ }
483
+ }
484
+
485
+ updateAudioParams() {
486
+ if (!this.ctx) return;
487
+ if (this.filterNode) {
488
+ this.filterNode.frequency.setTargetAtTime(this.frequency, this.ctx.currentTime, 0.1);
489
+ }
490
+ if (this.gainNode) {
491
+ this.gainNode.gain.setTargetAtTime(this.volume, this.ctx.currentTime, 0.1);
492
+ }
493
+ }
494
+
495
+ updateUIActionState() {
496
+ if (this.isPlaying) {
497
+ this.playIcon.classList.add('tr__icon--hidden');
498
+ this.stopIcon.classList.remove('tr__icon--hidden');
499
+ } else {
500
+ this.playIcon.classList.remove('tr__icon--hidden');
501
+ this.stopIcon.classList.add('tr__icon--hidden');
502
+ }
503
+ }
504
+
505
+ generateNoise(data: Float32Array, type: string) {
506
+ const count = data.length;
507
+ if (type === 'white') {
508
+ for (let i = 0; i < count; i++) {
509
+ data[i] = Math.random() * 2 - 1;
510
+ }
511
+ } else if (type === 'pink') {
512
+ let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
513
+ for (let i = 0; i < count; i++) {
514
+ const white = Math.random() * 2 - 1;
515
+ b0 = 0.99886 * b0 + white * 0.0555179;
516
+ b1 = 0.99332 * b1 + white * 0.0750759;
517
+ b2 = 0.969 * b2 + white * 0.153852;
518
+ b3 = 0.8665 * b3 + white * 0.3104856;
519
+ b4 = 0.55 * b4 + white * 0.5329522;
520
+ b5 = -0.7616 * b5 - white * 0.016898;
521
+ data[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
522
+ data[i] *= 0.11;
523
+ b6 = white * 0.115926;
524
+ }
525
+ } else if (type === 'brown') {
526
+ let lastOut = 0;
527
+ for (let i = 0; i < count; i++) {
528
+ const white = Math.random() * 2 - 1;
529
+ data[i] = (lastOut + 0.02 * white) / 1.02;
530
+ lastOut = data[i];
531
+ data[i] *= 3.5;
532
+ }
533
+ }
534
+ }
535
+
536
+ resizeCanvas() {
537
+ this.canvas.width = this.canvas.offsetWidth;
538
+ this.canvas.height = this.canvas.offsetHeight;
539
+ }
540
+
541
+ drawPath(dataArray: Uint8Array, width: number, height: number, barWidth: number) {
542
+ let x = 0;
543
+ this.canvasCtx.beginPath();
544
+ this.canvasCtx.moveTo(0, height);
545
+ for (let i = 0; i < dataArray.length; i++) {
546
+ const y = height - (dataArray[i] / 255) * height;
547
+ if (i === 0) {
548
+ this.canvasCtx.moveTo(x, y);
549
+ } else {
550
+ this.canvasCtx.lineTo(x, y);
551
+ }
552
+ x += barWidth;
553
+ }
554
+ this.canvasCtx.lineTo(width, height);
555
+ }
556
+
557
+ draw() {
558
+ if (!this.isPlaying || !this.analyser) return;
559
+ this.animationId = requestAnimationFrame(() => this.draw());
560
+ const bufferLength = this.analyser.frequencyBinCount;
561
+ const dataArray = new Uint8Array(bufferLength);
562
+ this.analyser.getByteFrequencyData(dataArray);
563
+ const { width, height } = this.canvas;
564
+ this.canvasCtx.clearRect(0, 0, width, height);
565
+ this.drawPath(dataArray, width, height, (width / bufferLength) * 2.5);
566
+ this.canvasCtx.fillStyle = 'rgba(45, 212, 191, 0.2)';
567
+ this.canvasCtx.fill();
568
+ this.canvasCtx.strokeStyle = '#2dd4bf';
569
+ this.canvasCtx.lineWidth = 2;
570
+ this.canvasCtx.stroke();
571
+ }
572
+
573
+ clearCanvas() {
574
+ this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
575
+ }
576
+ }
577
+
578
+ document.addEventListener('DOMContentLoaded', () => {
579
+ new TinnitusReliever();
580
+ });
581
+ </script>