@jjlmoya/utils-audiovisual 1.2.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 (120) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +198 -0
  3. package/src/category/i18n/es.ts +198 -0
  4. package/src/category/i18n/fr.ts +198 -0
  5. package/src/category/index.ts +17 -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 +4 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -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/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/chromaticLens/bibliography.astro +17 -0
  23. package/src/tool/chromaticLens/component.astro +178 -0
  24. package/src/tool/chromaticLens/i18n/en.ts +246 -0
  25. package/src/tool/chromaticLens/i18n/es.ts +244 -0
  26. package/src/tool/chromaticLens/i18n/fr.ts +244 -0
  27. package/src/tool/chromaticLens/index.ts +43 -0
  28. package/src/tool/chromaticLens/logic.ts +87 -0
  29. package/src/tool/chromaticLens/seo.astro +15 -0
  30. package/src/tool/chromaticLens/style.css +308 -0
  31. package/src/tool/chromaticLens/ui.ts +109 -0
  32. package/src/tool/collageMaker/bibliography.astro +17 -0
  33. package/src/tool/collageMaker/component.astro +302 -0
  34. package/src/tool/collageMaker/i18n/en.ts +233 -0
  35. package/src/tool/collageMaker/i18n/es.ts +231 -0
  36. package/src/tool/collageMaker/i18n/fr.ts +231 -0
  37. package/src/tool/collageMaker/index.ts +51 -0
  38. package/src/tool/collageMaker/logic.ts +134 -0
  39. package/src/tool/collageMaker/seo.astro +15 -0
  40. package/src/tool/collageMaker/style.css +386 -0
  41. package/src/tool/exifCleaner/bibliography.astro +18 -0
  42. package/src/tool/exifCleaner/component.astro +162 -0
  43. package/src/tool/exifCleaner/i18n/en.ts +277 -0
  44. package/src/tool/exifCleaner/i18n/es.ts +277 -0
  45. package/src/tool/exifCleaner/i18n/fr.ts +277 -0
  46. package/src/tool/exifCleaner/index.ts +57 -0
  47. package/src/tool/exifCleaner/logic.ts +135 -0
  48. package/src/tool/exifCleaner/seo.astro +18 -0
  49. package/src/tool/exifCleaner/style.css +289 -0
  50. package/src/tool/exifCleaner/ui.ts +117 -0
  51. package/src/tool/imageCompressor/bibliography.astro +17 -0
  52. package/src/tool/imageCompressor/component.astro +262 -0
  53. package/src/tool/imageCompressor/i18n/en.ts +232 -0
  54. package/src/tool/imageCompressor/i18n/es.ts +230 -0
  55. package/src/tool/imageCompressor/i18n/fr.ts +230 -0
  56. package/src/tool/imageCompressor/index.ts +50 -0
  57. package/src/tool/imageCompressor/logic.ts +79 -0
  58. package/src/tool/imageCompressor/seo.astro +15 -0
  59. package/src/tool/imageCompressor/style.css +503 -0
  60. package/src/tool/printQualityCalculator/bibliography.astro +18 -0
  61. package/src/tool/printQualityCalculator/component.astro +318 -0
  62. package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
  63. package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
  64. package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
  65. package/src/tool/printQualityCalculator/index.ts +56 -0
  66. package/src/tool/printQualityCalculator/logic.ts +53 -0
  67. package/src/tool/printQualityCalculator/seo.astro +18 -0
  68. package/src/tool/printQualityCalculator/style.css +491 -0
  69. package/src/tool/printQualityCalculator/ui.ts +122 -0
  70. package/src/tool/privacyBlur/bibliography.astro +17 -0
  71. package/src/tool/privacyBlur/component.astro +230 -0
  72. package/src/tool/privacyBlur/i18n/en.ts +238 -0
  73. package/src/tool/privacyBlur/i18n/es.ts +236 -0
  74. package/src/tool/privacyBlur/i18n/fr.ts +236 -0
  75. package/src/tool/privacyBlur/index.ts +49 -0
  76. package/src/tool/privacyBlur/logic.ts +249 -0
  77. package/src/tool/privacyBlur/seo.astro +15 -0
  78. package/src/tool/privacyBlur/style.css +332 -0
  79. package/src/tool/privacyBlur/ui.ts +124 -0
  80. package/src/tool/subtitleSync/bibliography.astro +17 -0
  81. package/src/tool/subtitleSync/component.astro +187 -0
  82. package/src/tool/subtitleSync/i18n/en.ts +241 -0
  83. package/src/tool/subtitleSync/i18n/es.ts +241 -0
  84. package/src/tool/subtitleSync/i18n/fr.ts +241 -0
  85. package/src/tool/subtitleSync/index.ts +49 -0
  86. package/src/tool/subtitleSync/logic.ts +91 -0
  87. package/src/tool/subtitleSync/seo.astro +15 -0
  88. package/src/tool/subtitleSync/style.css +325 -0
  89. package/src/tool/subtitleSync/ui.ts +152 -0
  90. package/src/tool/timelapseCalculator/bibliography.astro +15 -0
  91. package/src/tool/timelapseCalculator/component.astro +148 -0
  92. package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
  93. package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
  94. package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
  95. package/src/tool/timelapseCalculator/index.ts +52 -0
  96. package/src/tool/timelapseCalculator/logic.ts +46 -0
  97. package/src/tool/timelapseCalculator/seo.astro +18 -0
  98. package/src/tool/timelapseCalculator/style.css +285 -0
  99. package/src/tool/tvDistance/bibliography.astro +17 -0
  100. package/src/tool/tvDistance/component.astro +178 -0
  101. package/src/tool/tvDistance/i18n/en.ts +223 -0
  102. package/src/tool/tvDistance/i18n/es.ts +223 -0
  103. package/src/tool/tvDistance/i18n/fr.ts +223 -0
  104. package/src/tool/tvDistance/index.ts +49 -0
  105. package/src/tool/tvDistance/logic.ts +47 -0
  106. package/src/tool/tvDistance/seo.astro +15 -0
  107. package/src/tool/tvDistance/style.css +435 -0
  108. package/src/tool/tvDistance/ui.ts +66 -0
  109. package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
  110. package/src/tool/videoFrameExtractor/component.astro +285 -0
  111. package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
  112. package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
  113. package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
  114. package/src/tool/videoFrameExtractor/index.ts +53 -0
  115. package/src/tool/videoFrameExtractor/logic.ts +49 -0
  116. package/src/tool/videoFrameExtractor/seo.astro +15 -0
  117. package/src/tool/videoFrameExtractor/style.css +426 -0
  118. package/src/tool/videoFrameExtractor/ui.ts +179 -0
  119. package/src/tools.ts +25 -0
  120. package/src/types.ts +72 -0
@@ -0,0 +1,285 @@
1
+ .tlc-root {
2
+ width: 100%;
3
+ max-width: 64rem;
4
+ margin: 0 auto;
5
+ }
6
+
7
+ .tlc-grid {
8
+ display: grid;
9
+ grid-template-columns: 1fr 1fr;
10
+ border-radius: 1.5rem;
11
+ overflow: hidden;
12
+ border: 1px solid #e2e8f0;
13
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
14
+ }
15
+
16
+ .theme-dark .tlc-grid {
17
+ border-color: #334155;
18
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
19
+ }
20
+
21
+ @media (max-width: 768px) {
22
+ .tlc-grid {
23
+ grid-template-columns: 1fr;
24
+ }
25
+ }
26
+
27
+ /* ── Panel izquierdo ── */
28
+ .tlc-inputs-panel {
29
+ padding: 2rem 3rem;
30
+ background: #f8fafc;
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 2rem;
34
+ }
35
+
36
+ .theme-dark .tlc-inputs-panel {
37
+ background: rgba(15, 23, 42, 0.5);
38
+ }
39
+
40
+ @media (max-width: 768px) {
41
+ .tlc-inputs-panel {
42
+ padding: 2rem;
43
+ }
44
+ }
45
+
46
+ .tlc-panel-title {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.5rem;
50
+ font-size: 1.25rem;
51
+ font-weight: 900;
52
+ color: #1e293b;
53
+ margin: 0;
54
+ }
55
+
56
+ .theme-dark .tlc-panel-title {
57
+ color: #f8fafc;
58
+ }
59
+
60
+ .tlc-panel-icon {
61
+ width: 1.25rem;
62
+ height: 1.25rem;
63
+ color: #6366f1;
64
+ }
65
+
66
+ .tlc-fields {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 1.5rem;
70
+ }
71
+
72
+ .tlc-field-group {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 0.75rem;
76
+ }
77
+
78
+ .tlc-group-label {
79
+ font-size: 0.7rem;
80
+ font-weight: 700;
81
+ text-transform: uppercase;
82
+ letter-spacing: 0.08em;
83
+ color: #64748b;
84
+ }
85
+
86
+ .tlc-row {
87
+ display: flex;
88
+ gap: 1rem;
89
+ }
90
+
91
+ .tlc-field {
92
+ flex: 1;
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 0.25rem;
96
+ }
97
+
98
+ .tlc-sub-label {
99
+ font-size: 0.7rem;
100
+ color: #94a3b8;
101
+ margin-left: 0.25rem;
102
+ }
103
+
104
+ .tlc-input {
105
+ width: 100%;
106
+ background: #fff;
107
+ border: 2px solid #e2e8f0;
108
+ border-radius: 0.75rem;
109
+ padding: 0.75rem 1rem;
110
+ font-size: 1.5rem;
111
+ font-weight: 700;
112
+ color: #334155;
113
+ outline: none;
114
+ transition: border-color 0.15s, box-shadow 0.15s;
115
+ box-sizing: border-box;
116
+ }
117
+
118
+ .theme-dark .tlc-input {
119
+ background: #1e293b;
120
+ border-color: #334155;
121
+ color: #f8fafc;
122
+ }
123
+
124
+ .tlc-input-indigo:focus {
125
+ border-color: #6366f1;
126
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
127
+ }
128
+
129
+ .theme-dark .tlc-input-indigo:focus {
130
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
131
+ }
132
+
133
+ .tlc-input-pink:focus {
134
+ border-color: #ec4899;
135
+ box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.15);
136
+ }
137
+
138
+ .theme-dark .tlc-input-pink:focus {
139
+ box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.25);
140
+ }
141
+
142
+ .tlc-select-wrapper {
143
+ position: relative;
144
+ }
145
+
146
+ .tlc-select {
147
+ appearance: none;
148
+ cursor: pointer;
149
+ width: 100%;
150
+ }
151
+
152
+ .tlc-select-arrow {
153
+ position: absolute;
154
+ right: 1rem;
155
+ top: 50%;
156
+ transform: translateY(-50%);
157
+ width: 1.25rem;
158
+ height: 1.25rem;
159
+ color: #94a3b8;
160
+ pointer-events: none;
161
+ }
162
+
163
+ /* ── Panel derecho: gradiente ── */
164
+ .tlc-results-panel {
165
+ padding: 2rem 3rem;
166
+ background: linear-gradient(135deg, #4f46e5, #7c3aed);
167
+ color: #fff;
168
+ display: flex;
169
+ flex-direction: column;
170
+ justify-content: space-between;
171
+ gap: 2rem;
172
+ }
173
+
174
+ @media (max-width: 768px) {
175
+ .tlc-results-panel {
176
+ padding: 2rem;
177
+ }
178
+ }
179
+
180
+ .tlc-results-title {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 0.5rem;
184
+ font-size: 1.25rem;
185
+ font-weight: 900;
186
+ color: #c7d2fe;
187
+ margin: 0;
188
+ }
189
+
190
+ .tlc-results-icon {
191
+ width: 1.25rem;
192
+ height: 1.25rem;
193
+ color: #a5b4fc;
194
+ }
195
+
196
+ .tlc-interval-section {
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 0.5rem;
200
+ }
201
+
202
+ .tlc-interval-label {
203
+ font-size: 0.7rem;
204
+ font-weight: 700;
205
+ text-transform: uppercase;
206
+ letter-spacing: 0.1em;
207
+ color: #c7d2fe;
208
+ margin: 0;
209
+ }
210
+
211
+ .tlc-interval-value {
212
+ display: flex;
213
+ align-items: baseline;
214
+ gap: 0.5rem;
215
+ }
216
+
217
+ .tlc-big-number {
218
+ font-size: clamp(3.5rem, 8vw, 5rem);
219
+ font-weight: 900;
220
+ letter-spacing: -0.03em;
221
+ line-height: 1;
222
+ }
223
+
224
+ .tlc-big-unit {
225
+ font-size: 1.5rem;
226
+ font-weight: 700;
227
+ color: #a5b4fc;
228
+ }
229
+
230
+ .tlc-stats-grid {
231
+ display: grid;
232
+ grid-template-columns: 1fr 1fr;
233
+ gap: 1.5rem 2rem;
234
+ }
235
+
236
+ .tlc-stat {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: 0.25rem;
240
+ }
241
+
242
+ .tlc-stat-label {
243
+ font-size: 0.65rem;
244
+ font-weight: 700;
245
+ text-transform: uppercase;
246
+ letter-spacing: 0.1em;
247
+ color: #c7d2fe;
248
+ margin: 0;
249
+ }
250
+
251
+ .tlc-stat-value {
252
+ font-size: 1.875rem;
253
+ font-weight: 700;
254
+ font-variant-numeric: tabular-nums;
255
+ margin: 0;
256
+ line-height: 1.1;
257
+ }
258
+
259
+ .tlc-stat-value-sm {
260
+ font-size: 1.25rem;
261
+ color: #e0e7ff;
262
+ }
263
+
264
+ .tlc-rule-info {
265
+ display: flex;
266
+ align-items: flex-start;
267
+ gap: 1rem;
268
+ padding-top: 1.5rem;
269
+ border-top: 1px solid rgba(165, 180, 252, 0.3);
270
+ }
271
+
272
+ .tlc-info-icon {
273
+ width: 1.5rem;
274
+ height: 1.5rem;
275
+ color: #a5b4fc;
276
+ flex-shrink: 0;
277
+ margin-top: 0.1rem;
278
+ }
279
+
280
+ .tlc-info-text {
281
+ font-size: 0.8rem;
282
+ line-height: 1.5;
283
+ color: #c7d2fe;
284
+ margin: 0;
285
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { tvDistance } 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 tvDistance.i18n[locale]?.();
12
+ if (!content || !content.bibliography || content.bibliography.length === 0) return null;
13
+
14
+ const { bibliography, bibliographyTitle = 'Bibliografía' } = content;
15
+ ---
16
+
17
+ <SharedBibliography title={bibliographyTitle} links={bibliography} />
@@ -0,0 +1,178 @@
1
+ ---
2
+ import type { TvDistanceUI } from './index';
3
+ import './style.css';
4
+
5
+ interface Props {
6
+ ui: TvDistanceUI;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <div class="tvd-root" id="tv-distance-root" data-ui={JSON.stringify(ui)}>
13
+ <div class="tvd-card">
14
+
15
+ <div class="tvd-body">
16
+
17
+ <div class="tvd-left">
18
+
19
+ <div class="tvd-specs-block">
20
+ <h3 class="tvd-specs-title">
21
+ <svg viewBox="0 0 24 24" class="tvd-tv-icon" aria-hidden="true">
22
+ <path d="M8.16 3L6.75 4.41L9.34 7H4c-1.11 0-2 .89-2 2v10c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-5.34l2.59-2.59L15.84 3L12 6.84zM4 9h13v10H4zm15.5 0a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1m0 3a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1"/>
23
+ </svg>
24
+ {ui.specTitle}
25
+ </h3>
26
+
27
+ <div class="tvd-field">
28
+ <div class="tvd-field-row">
29
+ <span class="tvd-label">{ui.diagonalLabel}</span>
30
+ <span id="tvd-diagonal-display" class="tvd-diagonal-val">55"</span>
31
+ </div>
32
+ <input type="range" id="tvd-diagonal" min="32" max="100" value="55" class="tvd-slider" />
33
+ </div>
34
+
35
+ <div class="tvd-field">
36
+ <span class="tvd-label">{ui.resolutionLabel}</span>
37
+ <div class="tvd-res-grid">
38
+ <button class="tvd-res-btn" data-res="1080p">1080p</button>
39
+ <button class="tvd-res-btn tvd-res-btn-active" data-res="4k">4K</button>
40
+ <button class="tvd-res-btn" data-res="8k">8K</button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="tvd-thx-block">
46
+ <div class="tvd-thx-header">
47
+ <svg viewBox="0 0 24 24" class="tvd-thx-icon" aria-hidden="true">
48
+ <path d="M11 9h2V7h-2m1 13c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m0-18A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m-1 15h2v-6h-2z"/>
49
+ </svg>
50
+ <span class="tvd-thx-title">{ui.thxRecommendation}</span>
51
+ </div>
52
+ <p class="tvd-thx-desc">{ui.thxDescription}</p>
53
+ </div>
54
+
55
+ </div>
56
+
57
+ <div class="tvd-right">
58
+
59
+ <div class="tvd-sim-badge">
60
+ <span class="tvd-sim-dot"></span>
61
+ <span class="tvd-sim-badge-text">{ui.simulationBadge}</span>
62
+ </div>
63
+
64
+ <div class="tvd-sim-area">
65
+
66
+ <div id="tvd-tv-visual" class="tvd-tv-visual">
67
+ <div class="tvd-tv-screen">
68
+ <div class="tvd-screen-gradient"></div>
69
+ <svg viewBox="0 0 24 24" class="tvd-screen-ghost" aria-hidden="true">
70
+ <path d="M21 3H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h5v2h8v-2h5a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m0 14H3V5h18m-5 6l-7 4V7"/>
71
+ </svg>
72
+ <div class="tvd-screen-inset"></div>
73
+ </div>
74
+ <div class="tvd-stand-neck"></div>
75
+ <div class="tvd-stand-base"></div>
76
+ <div class="tvd-tv-tooltip">
77
+ <span id="tvd-width-display">—</span> {ui.tvWidthLabel}
78
+ </div>
79
+ </div>
80
+
81
+ <div id="tvd-line" class="tvd-distance-line">
82
+ <div class="tvd-line-dashed"></div>
83
+ </div>
84
+
85
+ <div class="tvd-person">
86
+ <div class="tvd-person-inner">
87
+ <div class="tvd-person-head"></div>
88
+ <div class="tvd-person-body"></div>
89
+ </div>
90
+ <div class="tvd-location-card">
91
+ <span class="tvd-location-label">{ui.userLocationLabel}</span>
92
+ <span id="tvd-distance-display" class="tvd-location-val">—</span>
93
+ </div>
94
+ </div>
95
+
96
+ </div>
97
+
98
+ <div class="tvd-stats">
99
+ <div class="tvd-stat">
100
+ <span class="tvd-stat-label">{ui.minLimitLabel}</span>
101
+ <span id="tvd-min" class="tvd-stat-val">—</span>
102
+ </div>
103
+ <div class="tvd-stat tvd-stat-opt">
104
+ <span class="tvd-stat-label">{ui.optimalLimitLabel}</span>
105
+ <span id="tvd-opt" class="tvd-stat-val">—</span>
106
+ </div>
107
+ <div class="tvd-stat">
108
+ <span class="tvd-stat-label">{ui.maxLimitLabel}</span>
109
+ <span id="tvd-max" class="tvd-stat-val">—</span>
110
+ </div>
111
+ </div>
112
+
113
+ </div>
114
+
115
+ </div>
116
+
117
+ </div>
118
+ </div>
119
+
120
+ <script>
121
+ import { calculateViewingDistance, getDimensionsFromDiagonal } from './logic';
122
+ import type { TVSpecs } from './logic';
123
+ import type { TvDistanceUI } from './index';
124
+
125
+ function init() {
126
+ const root = document.getElementById('tv-distance-root');
127
+ if (!root) return;
128
+
129
+ const labels = JSON.parse(root.dataset.ui ?? '{}') as TvDistanceUI;
130
+
131
+ const diagonalInput = root.querySelector('#tvd-diagonal') as HTMLInputElement;
132
+ const diagonalDisplay = root.querySelector('#tvd-diagonal-display') as HTMLElement;
133
+ const resBtns = root.querySelectorAll('.tvd-res-btn') as NodeListOf<HTMLButtonElement>;
134
+ const tvVisual = root.querySelector('#tvd-tv-visual') as HTMLElement;
135
+ const widthDisplay = root.querySelector('#tvd-width-display') as HTMLElement;
136
+ const lineEl = root.querySelector('#tvd-line') as HTMLElement;
137
+ const distanceDisplay = root.querySelector('#tvd-distance-display') as HTMLElement;
138
+ const minEl = root.querySelector('#tvd-min') as HTMLElement;
139
+ const optEl = root.querySelector('#tvd-opt') as HTMLElement;
140
+ const maxEl = root.querySelector('#tvd-max') as HTMLElement;
141
+
142
+ let currentRes: TVSpecs['resolution'] = '4k';
143
+
144
+ function update() {
145
+ const diagonal = parseInt(diagonalInput.value);
146
+ diagonalDisplay.textContent = `${diagonal}"`;
147
+
148
+ const specs: TVSpecs = { diagonalInches: diagonal, resolution: currentRes, aspectRatio: 16 / 9 };
149
+ const dist = calculateViewingDistance(specs);
150
+ const dims = getDimensionsFromDiagonal(diagonal);
151
+
152
+ minEl.textContent = `${dist.min.toFixed(2)}${labels.unitMeters}`;
153
+ optEl.textContent = `${dist.optimal.toFixed(2)}${labels.unitMeters}`;
154
+ maxEl.textContent = `${dist.max.toFixed(2)}${labels.unitMeters}`;
155
+ distanceDisplay.textContent = `${dist.optimal.toFixed(2)}${labels.unitMeters}`;
156
+ widthDisplay.textContent = String(Math.round(dims.width));
157
+
158
+ const scale = 1.8;
159
+ tvVisual.style.width = `${Math.round(dims.width * scale)}px`;
160
+
161
+ const lineH = Math.max(60, dist.optimal * 80);
162
+ lineEl.style.height = `${lineH}px`;
163
+
164
+ resBtns.forEach(b => b.classList.toggle('tvd-res-btn-active', b.dataset.res === currentRes));
165
+ }
166
+
167
+ diagonalInput.addEventListener('input', update);
168
+ resBtns.forEach(b => b.addEventListener('click', () => {
169
+ currentRes = b.dataset.res as TVSpecs['resolution'];
170
+ update();
171
+ }));
172
+
173
+ update();
174
+ }
175
+
176
+ init();
177
+ document.addEventListener('astro:page-load', init);
178
+ </script>
@@ -0,0 +1,223 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { TvDistanceUI, TvDistanceLocaleContent } from '../index';
3
+
4
+ const slug = 'tv-viewing-distance-calculator-thx-4k-optimal-screen';
5
+ const title = 'TV Distance Calculator - THX and 4K Optimal Screen';
6
+ const description = 'Calculate the ideal distance to watch your television based on its size and resolution. Optimize your Home Cinema with THX and SMPTE standards.';
7
+
8
+ const ui: TvDistanceUI = {
9
+ specTitle: "Specifications",
10
+ diagonalLabel: "Diagonal Size",
11
+ resolutionLabel: "Resolution",
12
+ thxRecommendation: "THX Recommendation",
13
+ thxDescription: "THX recommends a distance that covers a 40-degree viewing angle for an immersive cinematic experience.",
14
+ simulationBadge: "Real-Time Simulation",
15
+ tvWidthLabel: "cm wide",
16
+ userLocationLabel: "Your Location",
17
+ minLimitLabel: "Minimum Limit",
18
+ optimalLimitLabel: "Optimal Distance",
19
+ maxLimitLabel: "Maximum Limit",
20
+ unitMeters: "m",
21
+ unitCm: "cm"
22
+ };
23
+
24
+ const faq: TvDistanceLocaleContent['faq'] = [
25
+ {
26
+ question: "Why does resolution matter when calculating distance?",
27
+ answer: "At higher resolutions (like 4K or 8K), pixels are smaller. This allows you to sit closer without perceiving the pixel grid, increasing immersion without losing sharpness.",
28
+ },
29
+ {
30
+ question: "What is the 40-degree viewing angle?",
31
+ answer: "It is the standard recommended by THX so that the screen occupies a large part of your peripheral vision, similar to the experience of a professional cinema hall.",
32
+ },
33
+ {
34
+ question: "Can I sit further away than recommended?",
35
+ answer: "Yes, but you will lose the benefit of high resolutions. If you sit too far from a 4K TV, your eye won't be able to distinguish the extra details and you'll see it as if it were 1080p.",
36
+ },
37
+ ];
38
+
39
+ const howTo: TvDistanceLocaleContent['howTo'] = [
40
+ {
41
+ name: "Indicate inches",
42
+ text: "Move the slider to select the size of your current television or the one you plan to buy.",
43
+ },
44
+ {
45
+ name: "Select resolution",
46
+ text: "Choose between 1080p, 4K, or 8K to adjust the visual acuity limits.",
47
+ },
48
+ {
49
+ name: "Visualize simulation",
50
+ text: "Observe in the diagram how the size relationship between you and the screen changes.",
51
+ },
52
+ {
53
+ name: "Adjust your sofa",
54
+ text: "Place your seat within the 'Optimal Distance' range to maximize cinematic immersion.",
55
+ },
56
+ ];
57
+
58
+ const bibliography: TvDistanceLocaleContent['bibliography'] = [
59
+ {
60
+ name: "THX - HDTV Set Up Guide",
61
+ url: "https://www.thx.com/questions/what-is-the-best-viewing-distance-for-my-tv/",
62
+ },
63
+ {
64
+ name: "SMPTE - Standards Documentation",
65
+ url: "https://www.smpte.org/",
66
+ },
67
+ ];
68
+
69
+ const seo: TvDistanceLocaleContent['seo'] = [
70
+ {
71
+ type: 'summary',
72
+ title: 'Optimal TV Distance by Resolution and Size',
73
+ items: [
74
+ 'THX standards (40°) for immersive cinematic experience',
75
+ 'Calculations for 1080p, 4K, and 8K with maximum precision',
76
+ 'Real-time visual simulation of your setup',
77
+ 'Avoid eye fatigue and maximize content immersion'
78
+ ]
79
+ },
80
+ { type: 'title', text: 'Science of Immersion: Optimal TV Distance', level: 2 },
81
+ { type: 'paragraph', html: 'It\'s not just about comfort, but about human visual physiology. The eye has a resolution acuity limit; if you sit too far from a 4K screen, you are wasting money on pixels your vision cannot exploit. And if you sit too close, it causes eye fatigue.' },
82
+
83
+ { type: 'stats', items: [
84
+ { value: '40°', label: 'Ideal THX Angle', icon: 'mdi:eye' },
85
+ { value: '100%', label: 'Exploited Pixels', icon: 'mdi:video-high-definition' },
86
+ { value: 'Zero', label: 'Visual Fatigue', icon: 'mdi:heart-pulse' }
87
+ ], columns: 3 },
88
+
89
+ { type: 'title', text: 'Two Professional Standards: THX vs SMPTE', level: 3 },
90
+ { type: 'paragraph', html: 'There are two main authorities in the audiovisual industry that define how to optimize your viewing experience:' },
91
+ { type: 'comparative', items: [
92
+ {
93
+ title: 'THX Standard',
94
+ description: '40° viewing angle - More immersive',
95
+ icon: 'mdi:filmstrip',
96
+ points: [
97
+ 'Screen occupies more of your peripheral vision',
98
+ 'Immersive cinematic experience',
99
+ 'Ideal for action movies, live sports',
100
+ 'Requires more room space'
101
+ ],
102
+ highlight: true
103
+ },
104
+ {
105
+ title: 'SMPTE Standard',
106
+ description: '30° viewing angle - More relaxed',
107
+ icon: 'mdi:television',
108
+ points: [
109
+ 'More comfortable sitting distance',
110
+ 'Conservative professional recommendation',
111
+ 'Ideal for varied content (news, series)',
112
+ 'Better for smaller rooms'
113
+ ]
114
+ }
115
+ ], columns: 2 },
116
+
117
+ { type: 'title', text: 'How Resolution Changes the Equation', level: 3 },
118
+ { type: 'table', headers: ['Resolution', '55" TV Size', 'Minimum Distance (no visible pixels)', 'Optimal THX Distance'], rows: [
119
+ ['1080p (Full HD)', '55 inches', '2.0 m', '2.3 m'],
120
+ ['4K (Ultra HD)', '55 inches', '1.2 m', '1.5 m'],
121
+ ['8K', '55 inches', '0.8 m', '1.0 m']
122
+ ] },
123
+
124
+ { type: 'paragraph', html: 'As you see, higher resolution allows sitting closer without perceiving the pixel structure. A 4K TV can be appreciated from 1.2 meters away; a 1080p needs at least 2 meters.' },
125
+
126
+ { type: 'card', title: 'The Human Visual Acuity Limit', html: 'The human eye has a resolution limit capacity (visual acuity). Beyond a certain distance, you cannot distinguish more details even if the screen has 8K. Our calculations position you exactly in the "optimal zone" where you exploit 100% of the pixels without wasting unnecessary ocular energy.' },
127
+
128
+ { type: 'diagnostic', variant: 'info', title: 'Eye Fatigue and Distance', icon: 'mdi:information', badge: 'Visual Health', html: 'Being too close causes fatigue because your eyes need to constantly accommodate to extreme angles. Being too far causes strain to distinguish details. Our calculator positions you in the "sweet spot" where the screen fills your natural vision (40° for THX) without causing ocular stress.' },
129
+
130
+ { type: 'proscons', items: [
131
+ {
132
+ pro: 'THX 40° creates true cinematic immersion - like a professional cinema hall',
133
+ con: 'Requires more available space in the room'
134
+ },
135
+ {
136
+ pro: '4K allows sitting closer than 1080p - useful in small spaces',
137
+ con: 'You must be at minimum distance to exploit extra pixels'
138
+ },
139
+ {
140
+ pro: 'Avoid eye fatigue by positioning yourself correctly from the start',
141
+ con: 'Most home rooms DO NOT follow these standards (hence the fatigue)'
142
+ },
143
+ {
144
+ pro: 'You can validate if your current TV is well positioned',
145
+ con: 'If it is wrong, it requires moving furniture (not a software issue)'
146
+ }
147
+ ], proTitle: 'Advantages', conTitle: 'Limitations' },
148
+
149
+ { type: 'glossary', items: [
150
+ {
151
+ term: 'Viewing Angle (in degrees)',
152
+ definition: 'Visual angle the screen covers from your position. 40° = very immersive (peripheral covered); 20° = background content (not immersive).'
153
+ },
154
+ {
155
+ term: 'THX (Tomlinson Holman Experiment)',
156
+ definition: 'Audiovisual quality certification created by Lucasfilm. Defines standards for cinemas, home theater, and audio equipment. 40° is its primary recommendation.'
157
+ },
158
+ {
159
+ term: 'SMPTE (Society of Motion Picture Technical Engineers)',
160
+ definition: 'Technical standards organization for film and video. Recommends 30° as a conservative professional distance.'
161
+ },
162
+ {
163
+ term: 'Human Visual Acuity',
164
+ definition: 'Capability of the eye to distinguish fine details. An individual pixel is imperceptible past a certain distance (depends on resolution).'
165
+ },
166
+ {
167
+ term: '4K (Ultra HD)',
168
+ definition: 'Resolution of ~3840x2160 pixels. 4 times more pixels than 1080p. Allows getting closer without perceiving pixel structure.'
169
+ }
170
+ ] },
171
+
172
+ { type: 'message', title: 'Professional Audiovisual Room Optimization', ariaLabel: 'Information about TV setup standards', html: 'Professional cinemas spend millions ensuring distance, angle, and lighting are perfect. Our calculator brings those same principles to your home. It\'s not luxury: it\'s an investment in visual comfort and maximum exploitation of your equipment.' },
173
+
174
+ { type: 'title', text: 'Set Up Your Home Cinema Professionally', level: 3 },
175
+ { type: 'paragraph', html: 'Distance is JUST AS important as screen size and resolution. Follow these standards to transform your living room into an authentic cinematic hall. You\'ll see movies, series, and sports content with the immersion they were designed for.' }
176
+ ];
177
+
178
+ const faqSchema: WithContext<FAQPage> = {
179
+ '@context': 'https://schema.org',
180
+ '@type': 'FAQPage',
181
+ mainEntity: faq.map((item) => ({
182
+ '@type': 'Question',
183
+ name: item.question,
184
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
185
+ })),
186
+ };
187
+
188
+ const howToSchema: WithContext<HowTo> = {
189
+ '@context': 'https://schema.org',
190
+ '@type': 'HowTo',
191
+ name: title,
192
+ description,
193
+ step: howTo.map((step) => ({
194
+ '@type': 'HowToStep',
195
+ name: step.name,
196
+ text: step.text,
197
+ })),
198
+ };
199
+
200
+ const appSchema: WithContext<SoftwareApplication> = {
201
+ '@context': 'https://schema.org',
202
+ '@type': 'SoftwareApplication',
203
+ name: title,
204
+ description,
205
+ applicationCategory: 'UtilitiesApplication',
206
+ operatingSystem: 'Web',
207
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
208
+ inLanguage: 'en',
209
+ };
210
+
211
+ export const content: TvDistanceLocaleContent = {
212
+ slug,
213
+ title,
214
+ description,
215
+ ui,
216
+ seo,
217
+ faq,
218
+ faqTitle: 'Frequently Asked Questions about TV Viewing Distance',
219
+ bibliography,
220
+ bibliographyTitle: 'Professional Standards for Television Setup',
221
+ howTo,
222
+ schemas: [faqSchema as any, howToSchema as any, appSchema],
223
+ };