@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,36 @@
1
+ export interface CaffeineTrackerUI extends Record<string, string> {
2
+ drinkEspressoName: string;
3
+ drinkDoubleEspressoName: string;
4
+ drinkBrewedName: string;
5
+ drinkEnergySmallName: string;
6
+ drinkMonsterName: string;
7
+ drinkSodaName: string;
8
+ drinkTeaName: string;
9
+ drinkEspressoDesc: string;
10
+ drinkDoubleEspressoDesc: string;
11
+ drinkBrewedDesc: string;
12
+ drinkEnergySmallDesc: string;
13
+ drinkMonsterDesc: string;
14
+ drinkSodaDesc: string;
15
+ drinkTeaDesc: string;
16
+ metabolismFastLabel: string;
17
+ metabolismNormalLabel: string;
18
+ metabolismSlowLabel: string;
19
+ metabolismPregnancyLabel: string;
20
+ metabolismFastDesc: string;
21
+ metabolismNormalDesc: string;
22
+ metabolismSlowDesc: string;
23
+ metabolismPregnancyDesc: string;
24
+ sectionAddDrink: string;
25
+ sectionMetabolism: string;
26
+ sectionJournal: string;
27
+ labelCurrentMg: string;
28
+ labelEliminationTime: string;
29
+ labelSleepQuestion: string;
30
+ statusOptimal: string;
31
+ statusModerate: string;
32
+ statusCritical: string;
33
+ btnReset: string;
34
+ journalEmpty: string;
35
+ sleepAtPrefix: string;
36
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { daltonismSimulator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await daltonismSimulator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <Bibliography items={content.bibliography} title={content.bibliographyTitle} />}
@@ -0,0 +1,383 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { DaltonismSimulatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui ?? {}) as DaltonismSimulatorUI;
11
+ ---
12
+
13
+ <div
14
+ class="ds"
15
+ id="ds-root"
16
+ data-desc-normal={t.descNormal ?? 'Normal color vision. Full spectral perception.'}
17
+ data-desc-protanopia={t.descProtanopia ?? 'Absent L (red) cones. Reds appear dark or black.'}
18
+ data-desc-deuteranopia={t.descDeuteranopia ?? 'Absent M (green) cones. Reds and greens are indistinguishable.'}
19
+ data-desc-tritanopia={t.descTritanopia ?? 'Absent S (blue) cones. Blues appear greenish.'}
20
+ data-desc-protanomaly={t.descProtanomaly ?? 'Deficient L cones. Reds are less saturated.'}
21
+ data-desc-deuteranomaly={t.descDeuteranomaly ?? 'Deficient M cones. Most common type.'}
22
+ data-desc-achromatopsia={t.descAchromatopsia ?? 'Total absence of color perception. Only shades of grey.'}
23
+ >
24
+ <div class="ds__types">
25
+ <button class="ds__btn ds__btn--active" data-type="normal">{t.typeNormal ?? 'Normal'}</button>
26
+ <button class="ds__btn" data-type="protanopia">{t.typeProtanopia ?? 'Protanopia'}</button>
27
+ <button class="ds__btn" data-type="deuteranopia">{t.typeDeuteranopia ?? 'Deuteranopia'}</button>
28
+ <button class="ds__btn" data-type="tritanopia">{t.typeTritanopia ?? 'Tritanopia'}</button>
29
+ <button class="ds__btn" data-type="protanomaly">{t.typeProtanomaly ?? 'Protanomaly'}</button>
30
+ <button class="ds__btn" data-type="deuteranomaly">{t.typeDeuteranomaly ?? 'Deuteranomaly'}</button>
31
+ <button class="ds__btn" data-type="achromatopsia">{t.typeAchromatopsia ?? 'Achromatopsia'}</button>
32
+ </div>
33
+
34
+ <p class="ds__type-desc" id="ds-type-desc">{t.descNormal ?? 'Normal color vision.'}</p>
35
+
36
+ <div class="ds__comparison">
37
+ <div class="ds__panel">
38
+ <span class="ds__panel-label">{t.labelOriginal ?? 'Original'}</span>
39
+ <canvas class="ds__canvas" id="ds-canvas-orig" width="480" height="300"></canvas>
40
+ </div>
41
+ <div class="ds__panel">
42
+ <span class="ds__panel-label ds__panel-label--sim" id="ds-sim-label">{t.typeNormal ?? 'Normal'}</span>
43
+ <canvas class="ds__canvas" id="ds-canvas-sim" width="480" height="300"></canvas>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="ds__upload" id="ds-upload">
48
+ <label for="ds-file" class="ds__upload-label">{t.btnUpload ?? 'Upload image'}</label>
49
+ <span class="ds__upload-or">{t.orDrag ?? 'or drag here'}</span>
50
+ <input type="file" id="ds-file" accept="image/*" class="ds__file-input">
51
+ </div>
52
+ </div>
53
+
54
+ <style>
55
+ .ds {
56
+ --ds-accent: #7c3aed;
57
+ --ds-bg: rgba(255, 255, 255, 0.95);
58
+ --ds-border: rgba(226, 232, 240, 0.8);
59
+ --ds-text: #1e293b;
60
+ --ds-muted: #64748b;
61
+ --ds-btn-bg: rgba(248, 250, 252, 0.9);
62
+ --ds-btn-border: rgba(226, 232, 240, 0.8);
63
+ --ds-canvas-bg: #f1f5f9;
64
+ --ds-upload-bg: rgba(248, 250, 252, 0.5);
65
+
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 1.5rem;
69
+ background: var(--ds-bg);
70
+ border: 1px solid var(--ds-border);
71
+ border-radius: 2rem;
72
+ padding: 2rem;
73
+ max-width: 960px;
74
+ margin: 2rem auto;
75
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
76
+ }
77
+
78
+ :global(.theme-dark) .ds {
79
+ --ds-bg: rgba(15, 23, 42, 0.9);
80
+ --ds-border: rgba(51, 65, 85, 0.8);
81
+ --ds-text: #f8fafc;
82
+ --ds-muted: #94a3b8;
83
+ --ds-btn-bg: rgba(30, 41, 59, 0.8);
84
+ --ds-btn-border: rgba(51, 65, 85, 0.8);
85
+ --ds-canvas-bg: #1e293b;
86
+ --ds-upload-bg: rgba(30, 41, 59, 0.5);
87
+ }
88
+
89
+ .ds__types {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ gap: 0.5rem;
93
+ }
94
+
95
+ .ds__btn {
96
+ padding: 0.45rem 1rem;
97
+ border-radius: 2rem;
98
+ border: 1px solid var(--ds-btn-border);
99
+ background: var(--ds-btn-bg);
100
+ color: var(--ds-muted);
101
+ font-size: 0.8rem;
102
+ font-weight: 600;
103
+ cursor: pointer;
104
+ transition: all 0.15s;
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .ds__btn:hover {
109
+ border-color: var(--ds-accent);
110
+ color: var(--ds-accent);
111
+ }
112
+
113
+ .ds__btn--active {
114
+ background: var(--ds-accent);
115
+ border-color: var(--ds-accent);
116
+ color: #fff;
117
+ }
118
+
119
+ .ds__btn--active:hover {
120
+ color: #fff;
121
+ }
122
+
123
+ .ds__type-desc {
124
+ font-size: 0.875rem;
125
+ color: var(--ds-muted);
126
+ margin: 0;
127
+ min-height: 1.4em;
128
+ transition: color 0.2s;
129
+ }
130
+
131
+ .ds__comparison {
132
+ display: grid;
133
+ grid-template-columns: 1fr 1fr;
134
+ gap: 1rem;
135
+ }
136
+
137
+ .ds__panel {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 0.5rem;
141
+ }
142
+
143
+ .ds__panel-label {
144
+ font-size: 0.7rem;
145
+ font-weight: 800;
146
+ text-transform: uppercase;
147
+ letter-spacing: 0.1em;
148
+ color: var(--ds-muted);
149
+ }
150
+
151
+ .ds__panel-label--sim {
152
+ color: var(--ds-accent);
153
+ }
154
+
155
+ .ds__canvas {
156
+ width: 100%;
157
+ height: auto;
158
+ display: block;
159
+ border-radius: 0.75rem;
160
+ background: var(--ds-canvas-bg);
161
+ border: 1px solid var(--ds-border);
162
+ }
163
+
164
+ .ds__upload {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 1rem;
168
+ padding: 1rem 1.25rem;
169
+ border: 1px dashed var(--ds-btn-border);
170
+ border-radius: 1rem;
171
+ background: var(--ds-upload-bg);
172
+ transition: border-color 0.2s;
173
+ flex-wrap: wrap;
174
+ }
175
+
176
+ .ds__upload--drag {
177
+ border-color: var(--ds-accent);
178
+ }
179
+
180
+ .ds__upload-label {
181
+ display: inline-block;
182
+ padding: 0.5rem 1.25rem;
183
+ background: var(--ds-accent);
184
+ color: #fff;
185
+ border-radius: 2rem;
186
+ font-size: 0.825rem;
187
+ font-weight: 700;
188
+ cursor: pointer;
189
+ transition: opacity 0.15s;
190
+ }
191
+
192
+ .ds__upload-label:hover {
193
+ opacity: 0.85;
194
+ }
195
+
196
+ .ds__upload-or {
197
+ font-size: 0.825rem;
198
+ color: var(--ds-muted);
199
+ }
200
+
201
+ .ds__file-input {
202
+ display: none;
203
+ }
204
+
205
+ @media (max-width: 640px) {
206
+ .ds {
207
+ padding: 1.25rem;
208
+ border-radius: 1.5rem;
209
+ margin: 1rem 0;
210
+ gap: 1.25rem;
211
+ }
212
+
213
+ .ds__comparison {
214
+ grid-template-columns: 1fr;
215
+ }
216
+
217
+ .ds__btn {
218
+ font-size: 0.75rem;
219
+ padding: 0.4rem 0.85rem;
220
+ }
221
+ }
222
+ </style>
223
+
224
+ <script>
225
+ const root = document.getElementById('ds-root') as HTMLElement;
226
+ const ds = root?.dataset ?? {};
227
+
228
+ const origCanvas = document.getElementById('ds-canvas-orig') as HTMLCanvasElement;
229
+ const simCanvas = document.getElementById('ds-canvas-sim') as HTMLCanvasElement;
230
+ const typeDesc = document.getElementById('ds-type-desc') as HTMLElement;
231
+ const simLabel = document.getElementById('ds-sim-label') as HTMLElement;
232
+ const fileInput = document.getElementById('ds-file') as HTMLInputElement;
233
+ const uploadArea = document.getElementById('ds-upload') as HTMLElement;
234
+ const typeBtns = root?.querySelectorAll<HTMLButtonElement>('.ds__btn') ?? [];
235
+
236
+ let currentType = 'normal';
237
+
238
+ const MATRICES: Record<string, number[]> = {
239
+ normal: [1, 0, 0, 0, 1, 0, 0, 0, 1],
240
+ protanopia: [0.567, 0.433, 0, 0.558, 0.442, 0, 0, 0.242, 0.758],
241
+ deuteranopia: [0.625, 0.375, 0, 0.700, 0.300, 0, 0, 0.300, 0.700],
242
+ tritanopia: [0.950, 0.050, 0, 0, 0.433, 0.567, 0, 0.475, 0.525],
243
+ protanomaly: [0.817, 0.183, 0, 0.333, 0.667, 0, 0, 0.125, 0.875],
244
+ deuteranomaly: [0.800, 0.200, 0, 0.258, 0.742, 0, 0, 0.142, 0.858],
245
+ achromatopsia: [0.299, 0.587, 0.114, 0.299, 0.587, 0.114, 0.299, 0.587, 0.114],
246
+ };
247
+
248
+ const DESC_MAP: Record<string, string> = {
249
+ normal: ds.descNormal ?? '',
250
+ protanopia: ds.descProtanopia ?? '',
251
+ deuteranopia: ds.descDeuteranopia ?? '',
252
+ tritanopia: ds.descTritanopia ?? '',
253
+ protanomaly: ds.descProtanomaly ?? '',
254
+ deuteranomaly: ds.descDeuteranomaly ?? '',
255
+ achromatopsia: ds.descAchromatopsia ?? '',
256
+ };
257
+
258
+ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
259
+ const sl = s / 100;
260
+ const ll = l / 100;
261
+ const k = (n: number) => (n + h / 30) % 12;
262
+ const a = sl * Math.min(ll, 1 - ll);
263
+ const f = (n: number) => ll - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
264
+ return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
265
+ }
266
+
267
+ function drawPatches(ctx: CanvasRenderingContext2D, w: number, h: number): void {
268
+ const COLORS = [
269
+ '#e53e3e', '#dd6b20', '#d69e2e', '#38a169',
270
+ '#3182ce', '#805ad5', '#d53f8c', '#2d3748',
271
+ '#e2e8f0', '#f6e05e', '#68d391', '#90cdf4',
272
+ ];
273
+ const cw = w / 4;
274
+ const ch = (h * 0.55) / 3;
275
+ const sy = h * 0.45;
276
+ COLORS.forEach((color, i) => {
277
+ ctx.fillStyle = color;
278
+ ctx.fillRect((i % 4) * cw + 2, sy + Math.floor(i / 4) * ch + 2, cw - 4, ch - 4);
279
+ });
280
+ }
281
+
282
+ function drawSample(ctx: CanvasRenderingContext2D): void {
283
+ const w = ctx.canvas.width;
284
+ const h = ctx.canvas.height;
285
+ const stripH = Math.round(h * 0.45);
286
+ for (let x = 0; x < w; x++) {
287
+ const [r, g, b] = hslToRgb((x / w) * 360, 100, 50);
288
+ ctx.fillStyle = `rgb(${r},${g},${b})`;
289
+ ctx.fillRect(x, 0, 1, stripH);
290
+ }
291
+ drawPatches(ctx, w, h);
292
+ }
293
+
294
+ function applyMatrix(data: Uint8ClampedArray, m: number[]): void {
295
+ for (let i = 0; i < data.length; i += 4) {
296
+ const r = data[i];
297
+ const g = data[i + 1];
298
+ const b = data[i + 2];
299
+ data[i] = Math.min(255, Math.round(m[0] * r + m[1] * g + m[2] * b));
300
+ data[i + 1] = Math.min(255, Math.round(m[3] * r + m[4] * g + m[5] * b));
301
+ data[i + 2] = Math.min(255, Math.round(m[6] * r + m[7] * g + m[8] * b));
302
+ }
303
+ }
304
+
305
+ function renderSim(type: string): void {
306
+ const origCtx = origCanvas.getContext('2d');
307
+ const simCtx = simCanvas.getContext('2d');
308
+ if (!origCtx || !simCtx) return;
309
+ simCanvas.width = origCanvas.width;
310
+ simCanvas.height = origCanvas.height;
311
+ simCtx.drawImage(origCanvas, 0, 0);
312
+ const imgData = simCtx.getImageData(0, 0, simCanvas.width, simCanvas.height);
313
+ applyMatrix(imgData.data, MATRICES[type] ?? MATRICES['normal']);
314
+ simCtx.putImageData(imgData, 0, 0);
315
+ }
316
+
317
+ function selectType(type: string, label: string): void {
318
+ currentType = type;
319
+ typeBtns.forEach((btn) => {
320
+ btn.classList.toggle('ds__btn--active', btn.dataset.type === type);
321
+ });
322
+ typeDesc.textContent = DESC_MAP[type] ?? '';
323
+ simLabel.textContent = label;
324
+ renderSim(type);
325
+ }
326
+
327
+ function loadImage(img: HTMLImageElement): void {
328
+ const origCtx = origCanvas.getContext('2d');
329
+ if (!origCtx) return;
330
+ const scaleW = origCanvas.width / img.width;
331
+ const scaleH = 400 / img.height;
332
+ const scale = Math.min(scaleW, scaleH, 1);
333
+ const w = Math.round(img.width * scale);
334
+ const h = Math.round(img.height * scale);
335
+ origCanvas.width = w;
336
+ origCanvas.height = h;
337
+ origCtx.drawImage(img, 0, 0, w, h);
338
+ renderSim(currentType);
339
+ }
340
+
341
+ function processFile(file: File): void {
342
+ const reader = new FileReader();
343
+ reader.onload = (e) => {
344
+ const img = new Image();
345
+ img.onload = () => loadImage(img);
346
+ img.src = e.target?.result as string;
347
+ };
348
+ reader.readAsDataURL(file);
349
+ }
350
+
351
+ typeBtns.forEach((btn) => {
352
+ btn.addEventListener('click', () => {
353
+ selectType(btn.dataset.type ?? 'normal', btn.textContent ?? '');
354
+ });
355
+ });
356
+
357
+ fileInput.addEventListener('change', () => {
358
+ const file = fileInput.files?.[0];
359
+ if (file) processFile(file);
360
+ });
361
+
362
+ uploadArea.addEventListener('dragover', (e) => {
363
+ e.preventDefault();
364
+ uploadArea.classList.add('ds__upload--drag');
365
+ });
366
+
367
+ uploadArea.addEventListener('dragleave', () => {
368
+ uploadArea.classList.remove('ds__upload--drag');
369
+ });
370
+
371
+ uploadArea.addEventListener('drop', (e) => {
372
+ e.preventDefault();
373
+ uploadArea.classList.remove('ds__upload--drag');
374
+ const file = (e as DragEvent).dataTransfer?.files[0];
375
+ if (file) processFile(file);
376
+ });
377
+
378
+ const origCtx = origCanvas.getContext('2d');
379
+ if (origCtx) {
380
+ drawSample(origCtx);
381
+ renderSim(currentType);
382
+ }
383
+ </script>
@@ -0,0 +1,188 @@
1
+ import type { WithContext, SoftwareApplication, FAQPage, HowToThing } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { DaltonismSimulatorUI } from '../ui';
4
+
5
+ const slug = 'color-blindness-simulator';
6
+ const title = 'Color Blindness Simulator: Visualize Color Vision Deficiency';
7
+ const description =
8
+ 'Simulate how people with color blindness perceive color. Visualize protanopia, deuteranopia, tritanopia and more with your own image or the included sample palette.';
9
+
10
+ const faqData = [
11
+ {
12
+ question: 'What is color blindness?',
13
+ answer:
14
+ 'Color blindness is a deficiency in color perception caused by the absence or dysfunction of one or more types of cone cells in the retina. Cones are photoreceptor cells responsible for detecting specific wavelengths of light. The most common form affects the ability to distinguish between red and green.',
15
+ },
16
+ {
17
+ question: 'How many people are color blind?',
18
+ answer:
19
+ 'Approximately 8% of men and 0.5% of women of European descent have some form of color vision deficiency. Globally, this affects around 300 million people worldwide.',
20
+ },
21
+ {
22
+ question: 'What is the difference between protanopia and deuteranopia?',
23
+ answer:
24
+ 'Protanopia involves the complete absence of L cones (red-sensitive), so red tones appear dark or black. Deuteranopia involves the absence of M cones (green-sensitive), causing reds and greens to be indistinguishable, although they do not necessarily appear dark.',
25
+ },
26
+ {
27
+ question: 'Is there a cure for color blindness?',
28
+ answer:
29
+ 'There is no established medical cure. However, glasses and contact lenses with special filters can help some people distinguish certain colors better. Digital applications and accessible design are the best support tools currently available.',
30
+ },
31
+ ];
32
+
33
+ const howToData = [
34
+ {
35
+ name: 'Select the type of color blindness',
36
+ text: 'Click one of the type buttons: Normal, Protanopia, Deuteranopia, Tritanopia, Protanomaly, Deuteranomaly or Achromatopsia.',
37
+ },
38
+ {
39
+ name: 'Observe the color sample',
40
+ text: 'The tool automatically shows a reference color palette alongside the simulation of the selected type.',
41
+ },
42
+ {
43
+ name: 'Upload your own image',
44
+ text: 'Click "Upload image" or drag an image file to simulate how people with each type of color blindness would perceive it.',
45
+ },
46
+ {
47
+ name: 'Compare original and simulated',
48
+ text: 'The split view shows the original image on the left and the simulation on the right for direct comparison.',
49
+ },
50
+ ];
51
+
52
+ const faqSchema: WithContext<FAQPage> = {
53
+ '@context': 'https://schema.org',
54
+ '@type': 'FAQPage',
55
+ mainEntity: faqData.map((item) => ({
56
+ '@type': 'Question',
57
+ name: item.question,
58
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
59
+ })),
60
+ };
61
+
62
+ const howToSchema: WithContext<HowToThing> = {
63
+ '@context': 'https://schema.org',
64
+ '@type': 'HowTo',
65
+ name: title,
66
+ description,
67
+ step: howToData.map((step, i) => ({
68
+ '@type': 'HowToStep',
69
+ position: i + 1,
70
+ name: step.name,
71
+ text: step.text,
72
+ })),
73
+ };
74
+
75
+ const appSchema: WithContext<SoftwareApplication> = {
76
+ '@context': 'https://schema.org',
77
+ '@type': 'SoftwareApplication',
78
+ name: title,
79
+ description,
80
+ applicationCategory: 'HealthApplication',
81
+ operatingSystem: 'Web',
82
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
83
+ };
84
+
85
+ export const content: ToolLocaleContent<DaltonismSimulatorUI> = {
86
+ slug,
87
+ title,
88
+ description,
89
+ faqTitle: 'Frequently asked questions about color blindness',
90
+ faq: faqData,
91
+ bibliographyTitle: 'Scientific references',
92
+ bibliography: [
93
+ {
94
+ name: 'Machado GM, Oliveira MM, Fernandes LAF (2009). A Physiologically-based Model for Simulation of Color Vision Deficiency',
95
+ url: 'https://doi.org/10.1109/TVCG.2009.113',
96
+ },
97
+ {
98
+ name: 'Brettel H, Vienot F, Mollon JD (1997). Computerized simulation of color appearance for dichromats',
99
+ url: 'https://doi.org/10.1364/JOSAA.14.002647',
100
+ },
101
+ {
102
+ name: 'Color Blind Awareness — Global statistics on color vision deficiency',
103
+ url: 'https://www.colourblindawareness.org/colour-blindness/',
104
+ },
105
+ {
106
+ name: 'WCAG 2.1 — Success Criterion 1.4.3: Contrast (Minimum)',
107
+ url: 'https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html',
108
+ },
109
+ ],
110
+ howTo: howToData,
111
+ schemas: [faqSchema, howToSchema, appSchema],
112
+ ui: {
113
+ labelOriginal: 'Original',
114
+ labelSimulated: 'Simulated',
115
+ btnUpload: 'Upload image',
116
+ orDrag: 'or drag here',
117
+ typeNormal: 'Normal',
118
+ typeProtanopia: 'Protanopia',
119
+ typeDeuteranopia: 'Deuteranopia',
120
+ typeTritanopia: 'Tritanopia',
121
+ typeProtanomaly: 'Protanomaly',
122
+ typeDeuteranomaly: 'Deuteranomaly',
123
+ typeAchromatopsia: 'Achromatopsia',
124
+ descNormal: 'Normal color vision. Full perception of the visible spectrum.',
125
+ descProtanopia: 'Absent L (red) cones. Red tones appear dark or black.',
126
+ descDeuteranopia: 'Absent M (green) cones. Reds and greens are indistinguishable.',
127
+ descTritanopia: 'Absent S (blue) cones. Blues appear greenish or pinkish.',
128
+ descProtanomaly: 'Deficient L cones. Reds are less saturated and harder to distinguish.',
129
+ descDeuteranomaly: 'Deficient M cones. Most common type, affects ~6% of men.',
130
+ descAchromatopsia: 'Total absence of color perception. Only shades of grey are perceived.',
131
+ },
132
+ seo: [
133
+ {
134
+ type: 'summary',
135
+ title: 'Color Blindness Simulator Online',
136
+ items: [
137
+ '<strong>7 types simulated</strong>: Normal, Protanopia, Deuteranopia, Tritanopia, Protanomaly, Deuteranomaly and Achromatopsia.',
138
+ '<strong>Custom image</strong>: Upload your own photo or use the included color sample palette.',
139
+ '<strong>Split view</strong>: Original vs. simulated side by side in real time.',
140
+ '<strong>No data sent</strong>: All processing happens in your browser, no servers involved.',
141
+ ],
142
+ },
143
+ { type: 'title', text: 'What is color blindness and how does it affect vision?', level: 2 },
144
+ {
145
+ type: 'paragraph',
146
+ html: '<strong>Color blindness</strong>, also called <strong>color vision deficiency</strong> or <strong>dyschromatopsia</strong>, is a condition that affects the ability to perceive certain colors correctly. It was first described by the English chemist <strong>John Dalton</strong> in 1798, who observed that he and his brother could not distinguish certain colors, especially reds and greens.',
147
+ },
148
+ {
149
+ type: 'paragraph',
150
+ html: 'The condition is caused by alterations in the <strong>cone cells of the retina</strong>, the photoreceptor cells responsible for capturing different wavelengths of light. The human eye has three types of cones: L cones (red-sensitive, ~570 nm), M cones (green-sensitive, ~530 nm) and S cones (blue-sensitive, ~420 nm). When one or more types are absent or function deficiently, color perception is altered.',
151
+ },
152
+ { type: 'title', text: 'The 7 types of color vision deficiency', level: 2 },
153
+ {
154
+ type: 'table',
155
+ headers: ['Type', 'Affected Cone', 'Prevalence (men)', 'Main Effect'],
156
+ rows: [
157
+ ['Protanopia', 'L (red) absent', '1.01%', 'Darkened reds, red-green confusion'],
158
+ ['Protanomaly', 'L (red) deficient', '1.08%', 'Less saturated reds'],
159
+ ['Deuteranopia', 'M (green) absent', '1.27%', 'Red-green confusion without darkening'],
160
+ ['Deuteranomaly', 'M (green) deficient', '4.63%', 'Most common type, altered greens'],
161
+ ['Tritanopia', 'S (blue) absent', '<0.01%', 'Blue-green and yellow-red confusion'],
162
+ ['Tritanomaly', 'S (blue) deficient', '<0.01%', 'Altered blues and yellows'],
163
+ ['Achromatopsia', 'All cones', '0.003%', 'Greyscale perception only'],
164
+ ],
165
+ },
166
+ { type: 'title', text: 'How the color blindness simulation works', level: 2 },
167
+ {
168
+ type: 'paragraph',
169
+ html: 'This simulator applies <strong>color transformation matrices</strong> to the pixels of the image. Each pixel has RGB (red, green, blue) values that are mathematically transformed to simulate how a person with each type of deficiency would perceive them. The matrices used are based on research by Machado, Oliveira and Fernandes (2009) and the model by Brettel, Viénot and Mollon (1997), both widely validated in the scientific community.',
170
+ },
171
+ { type: 'title', text: 'Color blindness and accessible design', level: 2 },
172
+ {
173
+ type: 'list',
174
+ items: [
175
+ '<strong>Never use color alone to convey information:</strong> Add icons, patterns or explanatory text alongside any color coding.',
176
+ '<strong>Ensure sufficient contrast:</strong> WCAG 2.1 requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text.',
177
+ '<strong>Avoid red-green combinations:</strong> These are the most problematic for the majority of color blind people. Opt for blue-orange or blue-red instead.',
178
+ '<strong>Use simulation tools in your workflow:</strong> Integrate color blindness simulators into your design reviews to catch issues before publishing.',
179
+ '<strong>Test charts and maps:</strong> These visual elements are especially prone to accessibility issues due to their reliance on color.',
180
+ ],
181
+ },
182
+ {
183
+ type: 'tip',
184
+ title: 'Tool for designers and developers',
185
+ html: 'This simulator is especially useful for <strong>checking the accessibility</strong> of designs, UI screenshots, data charts or educational materials. Upload your design and check how users with protanopia or deuteranopia perceive it before publishing.',
186
+ },
187
+ ],
188
+ };