@jjlmoya/utils-hardware 1.18.0 → 1.20.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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/batteryHealthEstimator/component.astro +3 -3
  9. package/src/tool/gamepadTest/component.astro +4 -3
  10. package/src/tool/gamepadVibrationTester/component.astro +3 -3
  11. package/src/tool/keyboardTest/component.astro +6 -1
  12. package/src/tool/monitorGhostingTest/bibliography.astro +14 -0
  13. package/src/tool/monitorGhostingTest/bibliography.ts +20 -0
  14. package/src/tool/monitorGhostingTest/component.astro +156 -0
  15. package/src/tool/monitorGhostingTest/entry.ts +29 -0
  16. package/src/tool/monitorGhostingTest/i18n/de.ts +293 -0
  17. package/src/tool/monitorGhostingTest/i18n/en.ts +293 -0
  18. package/src/tool/monitorGhostingTest/i18n/es.ts +293 -0
  19. package/src/tool/monitorGhostingTest/i18n/fr.ts +293 -0
  20. package/src/tool/monitorGhostingTest/i18n/id.ts +293 -0
  21. package/src/tool/monitorGhostingTest/i18n/it.ts +293 -0
  22. package/src/tool/monitorGhostingTest/i18n/ja.ts +293 -0
  23. package/src/tool/monitorGhostingTest/i18n/ko.ts +293 -0
  24. package/src/tool/monitorGhostingTest/i18n/nl.ts +293 -0
  25. package/src/tool/monitorGhostingTest/i18n/pl.ts +293 -0
  26. package/src/tool/monitorGhostingTest/i18n/pt.ts +293 -0
  27. package/src/tool/monitorGhostingTest/i18n/ru.ts +293 -0
  28. package/src/tool/monitorGhostingTest/i18n/sv.ts +293 -0
  29. package/src/tool/monitorGhostingTest/i18n/tr.ts +293 -0
  30. package/src/tool/monitorGhostingTest/i18n/zh.ts +293 -0
  31. package/src/tool/monitorGhostingTest/index.ts +9 -0
  32. package/src/tool/monitorGhostingTest/logic.ts +195 -0
  33. package/src/tool/monitorGhostingTest/monitor-ghosting-test.css +546 -0
  34. package/src/tool/monitorGhostingTest/seo.astro +15 -0
  35. package/src/tool/monitorGhostingTest/ui.ts +30 -0
  36. package/src/tool/mouseDoubleClickTest/bibliography.astro +14 -0
  37. package/src/tool/mouseDoubleClickTest/bibliography.ts +16 -0
  38. package/src/tool/mouseDoubleClickTest/component.astro +135 -0
  39. package/src/tool/mouseDoubleClickTest/entry.ts +29 -0
  40. package/src/tool/mouseDoubleClickTest/i18n/de.ts +274 -0
  41. package/src/tool/mouseDoubleClickTest/i18n/en.ts +274 -0
  42. package/src/tool/mouseDoubleClickTest/i18n/es.ts +274 -0
  43. package/src/tool/mouseDoubleClickTest/i18n/fr.ts +274 -0
  44. package/src/tool/mouseDoubleClickTest/i18n/id.ts +285 -0
  45. package/src/tool/mouseDoubleClickTest/i18n/it.ts +274 -0
  46. package/src/tool/mouseDoubleClickTest/i18n/ja.ts +274 -0
  47. package/src/tool/mouseDoubleClickTest/i18n/ko.ts +274 -0
  48. package/src/tool/mouseDoubleClickTest/i18n/nl.ts +274 -0
  49. package/src/tool/mouseDoubleClickTest/i18n/pl.ts +274 -0
  50. package/src/tool/mouseDoubleClickTest/i18n/pt.ts +274 -0
  51. package/src/tool/mouseDoubleClickTest/i18n/ru.ts +274 -0
  52. package/src/tool/mouseDoubleClickTest/i18n/sv.ts +274 -0
  53. package/src/tool/mouseDoubleClickTest/i18n/tr.ts +274 -0
  54. package/src/tool/mouseDoubleClickTest/i18n/zh.ts +274 -0
  55. package/src/tool/mouseDoubleClickTest/index.ts +9 -0
  56. package/src/tool/mouseDoubleClickTest/logic.ts +258 -0
  57. package/src/tool/mouseDoubleClickTest/mouse-double-click-test.css +488 -0
  58. package/src/tool/mouseDoubleClickTest/seo.astro +15 -0
  59. package/src/tool/mouseDoubleClickTest/ui.ts +26 -0
  60. package/src/tool/mousePollingTest/logic/RatonManager.ts +6 -6
  61. package/src/tool/toneGenerator/component.astro +7 -7
  62. package/src/tools.ts +3 -2
@@ -0,0 +1,293 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { MonitorGhostingTestUI } from '../ui';
4
+ import { bibliography } from '../bibliography';
5
+
6
+ const slug = 'monitor-ghosting-test';
7
+ const title = '显示器拖影测试';
8
+ const description =
9
+ '使用移动条、文本和全屏运动模式测试显示器拖影、运动模糊、过冲拖尾和像素响应行为。';
10
+
11
+ const faqData = [
12
+ {
13
+ question: '什么是显示器拖影?',
14
+ answer:
15
+ '显示器拖影是当像素无法足够快地转换时,跟随移动物体出现的可见拖尾。这在慢速LCD面板、调试不当的过冲模式以及运行在最佳刷新率以下的显示器上很常见。',
16
+ },
17
+ {
18
+ question: '这个测试能测量精确的响应时间吗?',
19
+ answer:
20
+ '浏览器测试不能替代实验室设备,如追踪相机或光电二极管,但可以揭示可见的运动伪影,比较显示器设置,并帮助您选择最不模糊的过冲模式。',
21
+ },
22
+ {
23
+ question: '为什么过冲有时会产生明亮的拖尾?',
24
+ answer:
25
+ '过冲更用力地驱动像素以加速转换。如果超过目标色调,您可能会看到反向拖影:移动物体后面的明亮或彩色光晕。',
26
+ },
27
+ {
28
+ question: '我应该在深色还是浅色背景下测试?',
29
+ answer:
30
+ '两者都要。某些面板在深色到灰色的转换中比亮到暗的转换更容易产生拖影,因此改变背景可以揭示单一模式可能隐藏的伪影。',
31
+ },
32
+ ];
33
+
34
+ const howToData = [
35
+ {
36
+ name: '设置移动速度',
37
+ text: '从默认速度附近开始,然后逐渐增加,直到拖尾易于检查而不会跟丢物体。',
38
+ },
39
+ {
40
+ name: '改变拖尾强度',
41
+ text: '使用拖尾控件,在比较显示器设置时使残影更容易看到。',
42
+ },
43
+ {
44
+ name: '测试多种背景',
45
+ text: '在深色、浅色和网格背景之间切换,以揭示黑色污迹、反向拖影和过冲光晕。',
46
+ },
47
+ {
48
+ name: '比较过冲设置',
49
+ text: '打开显示器OSD,测试关闭、正常、快速和极限模式。选择运动最清晰、光晕最少的模式。',
50
+ },
51
+ ];
52
+
53
+ const faqSchema: WithContext<FAQPage> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'FAQPage',
56
+ mainEntity: faqData.map((item) => ({
57
+ '@type': 'Question',
58
+ name: item.question,
59
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
60
+ })),
61
+ };
62
+
63
+ const howToSchema: WithContext<HowTo> = {
64
+ '@context': 'https://schema.org',
65
+ '@type': 'HowTo',
66
+ name: title,
67
+ description,
68
+ step: howToData.map((step, i) => ({
69
+ '@type': 'HowToStep',
70
+ position: i + 1,
71
+ name: step.name,
72
+ text: step.text,
73
+ })),
74
+ };
75
+
76
+ const appSchema: WithContext<SoftwareApplication> = {
77
+ '@context': 'https://schema.org',
78
+ '@type': 'SoftwareApplication',
79
+ name: title,
80
+ description,
81
+ applicationCategory: 'UtilityApplication',
82
+ operatingSystem: 'All',
83
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
84
+ inLanguage: 'zh',
85
+ };
86
+
87
+ export const content: ToolLocaleContent<MonitorGhostingTestUI> = {
88
+ slug,
89
+ title,
90
+ description,
91
+ faq: faqData,
92
+ howTo: howToData,
93
+ schemas: [faqSchema, howToSchema, appSchema],
94
+ bibliography,
95
+ seo: [
96
+ {
97
+ type: 'title',
98
+ text: '显示器拖影测试:检查运动模糊和像素响应',
99
+ level: 2,
100
+ },
101
+ {
102
+ type: 'paragraph',
103
+ html: '显示器拖影在移动物体后面留下可见拖尾时出现。它会让游戏看起来模糊,使滚动文本更难阅读,让高刷新率显示器看起来比预期更差。这个显示器拖影测试为您提供移动条、文本和高对比度模式,以便比较过冲模式、刷新率、背景和移动速度,无需安装软件。',
104
+ },
105
+ {
106
+ type: 'paragraph',
107
+ html: '该测试专为实际检查而设计。它不声称为实验室级别的灰度响应时间,但有助于回答大多数用户真正的问题:<strong>在这个显示器上,哪个显示器设置在我的眼中看起来最清晰?</strong>',
108
+ },
109
+ {
110
+ type: 'title',
111
+ text: '拖影的样子',
112
+ level: 3,
113
+ },
114
+ {
115
+ type: 'list',
116
+ items: [
117
+ '跟随移动物体的暗影,通常称为黑色污迹',
118
+ '物体后面苍白或彩色的光晕,通常由过度过冲引起',
119
+ '使边缘看起来柔和的长条半透明拖尾',
120
+ '物体的多个微弱副本,特别是在像素响应慢的显示器上',
121
+ '深色、浅色和网格背景之间的不均匀清晰度',
122
+ ],
123
+ },
124
+ {
125
+ type: 'title',
126
+ text: '拖影、运动模糊和反向拖影',
127
+ level: 3,
128
+ },
129
+ {
130
+ type: 'table',
131
+ headers: ['伪影', '您看到的', '常见原因'],
132
+ rows: [
133
+ ['拖影', '较暗或褪色的副本跟随物体', '像素响应对于移动速度来说太慢'],
134
+ ['运动模糊', '整个移动物体看起来柔和', '采样保持模糊、低刷新率或眼球追踪模糊'],
135
+ ['反向拖影', '明亮光晕或彩色过冲拖尾', '过冲设置过于激进'],
136
+ ['黑色污迹', '暗部场景留下沉重阴影', 'VA面板暗色转换较慢'],
137
+ ['卡顿', '运动跳跃而非流畅', '帧节奏、低FPS或浏览器/系统负载'],
138
+ ],
139
+ },
140
+ {
141
+ type: 'title',
142
+ text: '实用的诊断工作流程',
143
+ level: 3,
144
+ },
145
+ {
146
+ type: 'paragraph',
147
+ html: '从将显示器设置为其原生分辨率和最高稳定刷新率开始。如果您拥有144Hz、165Hz、240Hz或360Hz显示器,在判断运动清晰度之前,请确认操作系统确实在使用该模式。全屏打开测试,选择清晰度条目标,并观察移动物体的后端。后端是虚影拖尾、暗色污迹和明亮过冲光晕最容易比较的地方。',
148
+ },
149
+ {
150
+ type: 'list',
151
+ items: [
152
+ '使用深色背景揭示黑色污迹和慢速暗色转换',
153
+ '使用浅色背景揭示彩色过冲光晕',
154
+ '使用网格背景对照高对比度参考线检查边缘清晰度',
155
+ '当您的真正问题是模糊滚动或移动中的文字难以阅读时,使用文本目标',
156
+ '在判断显示器之前使用全屏,因为浏览器边框和缩放可能会分散对运动伪影的注意力',
157
+ '只有在能够舒适地跟随物体之后再提高速度',
158
+ '一次只比较一个显示器设置,以了解发生了什么变化',
159
+ ],
160
+ },
161
+ {
162
+ type: 'title',
163
+ text: '为您的显示器选择最佳过冲设置',
164
+ level: 3,
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: '大多数游戏显示器包含名为关闭、正常、快速、更快、极限、响应时间或Trace Free的过冲设置。最快的选项并不总是最好的。适中的设置通常提供最佳平衡:比关闭更少的模糊,但比极限更少的光晕。',
169
+ },
170
+ {
171
+ type: 'table',
172
+ headers: ['过冲模式', '预期结果', '建议'],
173
+ rows: [
174
+ ['关闭', '过冲最少,但模糊更多', '用于比较的有用基线'],
175
+ ['正常', '适度的模糊减少', '通常最适合日常使用'],
176
+ ['快速', '更清晰的运动,但有一定光晕风险', '如果拖尾保持干净则适用'],
177
+ ['极限', '声称最低响应时间,过冲风险最高', '如果出现明亮反向拖尾请避免'],
178
+ ['自适应/可变', '行为随刷新率变化', '在您实际使用的FPS范围内重新测试'],
179
+ ],
180
+ },
181
+ {
182
+ type: 'title',
183
+ text: '测试效果不佳时的更改方法',
184
+ level: 3,
185
+ },
186
+ {
187
+ type: 'table',
188
+ headers: ['您看到的', '可能的原因', '尝试的方法'],
189
+ rows: [
190
+ ['目标后面的长条暗色拖尾', '慢速暗色像素转换或过冲较弱', '尝试更强的过冲模式,在深色和网格背景下重新测试'],
191
+ ['目标后面的明亮光晕或彩色轮廓', '过冲过度或反向拖影', '将过冲降低一级并在真实刷新率下比较'],
192
+ ['运动看起来跳跃而非模糊', '帧节奏、浏览器负载或刷新率不匹配', '关闭重负载应用,启用全屏,确认操作系统刷新率'],
193
+ ['移动中文字变得难以阅读', '采样保持模糊、低刷新率或响应慢', '提高刷新率,测试文字模式,比较过冲模式'],
194
+ ['FPS变化时伪影发生变化', 'VRR或自适应过冲行为', '在您实际游戏或工作的FPS范围内重新测试'],
195
+ ],
196
+ },
197
+ {
198
+ type: 'title',
199
+ text: '为什么刷新率很重要',
200
+ level: 3,
201
+ },
202
+ {
203
+ type: 'paragraph',
204
+ html: '更高的刷新率减少了每帧保持可见的时间,可以使运动看起来更清晰。然而,仅靠刷新率并不能消除拖影。像素转换慢的240Hz显示器仍然会拖影,而调校良好的144Hz面板可能比调校不佳的更快面板看起来更干净。',
205
+ },
206
+ {
207
+ type: 'table',
208
+ headers: ['刷新率', '帧时间', '预期效果'],
209
+ rows: [
210
+ ['60Hz', '16.7 ms', '容易看到采样保持模糊和较慢的运动'],
211
+ ['120Hz', '8.3 ms', '更流畅,但像素响应仍然重要'],
212
+ ['144Hz', '6.9 ms', '运动清晰度的常见游戏基线'],
213
+ ['240Hz', '4.2 ms', '如果响应调校良好则非常流畅'],
214
+ ['360Hz', '2.8 ms', '要求高:糟糕的过冲调校变得明显'],
215
+ ],
216
+ },
217
+ {
218
+ type: 'title',
219
+ text: 'VRR、游戏和真实环境测试',
220
+ level: 3,
221
+ },
222
+ {
223
+ type: 'paragraph',
224
+ html: '可变刷新率可能会改变显示器的行为,因为某些显示器在不同刷新率下使用不同的过冲调校。如果您的问题仅出现在游戏中,不要仅在桌面最大刷新率下测试。在您实际游戏的FPS范围内重新测试,特别是60 FPS、90 FPS、120 FPS附近以及您使用的任何帧率上限。',
225
+ },
226
+ {
227
+ type: 'list',
228
+ items: [
229
+ '如果在低FPS下拖影更严重,显示器可能具有较弱的可变过冲调校',
230
+ '如果光晕仅在高刷新率下出现,过冲模式可能过于激进',
231
+ '如果运动卡顿而拖尾保持较短,问题可能是帧节奏而非像素响应',
232
+ '如果全屏与窗口模式看起来不同,请检查浏览器缩放、操作系统缩放和合成器行为',
233
+ ],
234
+ },
235
+ {
236
+ type: 'title',
237
+ text: '糟糕结果的故障排除',
238
+ level: 3,
239
+ },
240
+ {
241
+ type: 'list',
242
+ items: [
243
+ '确认您的显示线缆支持目标刷新率',
244
+ '在比较正常过冲时禁用运动平滑、黑帧插入或MPRT模式',
245
+ '将显示器切换到其原生分辨率后重新测试',
246
+ '如果运动看起来不一致,使用全屏或降低浏览器缩放',
247
+ '如果动画卡顿,关闭重负载后台应用',
248
+ '更改GPU控制面板刷新率设置后测试相同模式',
249
+ '如果显示器无法达到其标称刷新率,尝试另一根线缆或端口',
250
+ '当拖影在桌面和游戏之间变化时,重新测试VRR开启和关闭',
251
+ ],
252
+ },
253
+ {
254
+ type: 'title',
255
+ text: '在线拖影测试的局限性',
256
+ level: 3,
257
+ },
258
+ {
259
+ type: 'paragraph',
260
+ html: '基于浏览器的拖影测试取决于浏览器动画时序、GPU负载、操作系统合成和显示配置。它非常适合视觉比较,但精确的响应时间测量需要专用设备,如追踪相机、光电二极管或基于示波器的方法。使用此测试来选择设置并发现明显的伪影,而不是用于认证制造商的响应时间声明。',
261
+ },
262
+ ],
263
+ ui: {
264
+ badge: '运动清晰度',
265
+ speedLabel: '移动速度',
266
+ pixelsPerSecondUnit: 'px/s',
267
+ pixelUnit: 'px',
268
+ millisecondUnit: 'ms',
269
+ trailLabel: '拖尾强度',
270
+ backgroundLabel: '背景',
271
+ backgroundDark: '深色',
272
+ backgroundLight: '浅色',
273
+ backgroundGrid: '网格',
274
+ patternLabel: '测试目标',
275
+ patternBars: '清晰度条',
276
+ patternText: '文本块',
277
+ patternUfo: 'UFO',
278
+ pursuitLabel: '追踪引导',
279
+ pursuitOn: '开启',
280
+ pursuitOff: '关闭',
281
+ fullscreen: '全屏',
282
+ exitFullscreen: '退出全屏',
283
+ pause: '暂停',
284
+ resume: '继续',
285
+ targetText: '运动',
286
+ estimatedBlur: '估计模糊',
287
+ frameStep: '帧步进',
288
+ persistence: '残影',
289
+ sampleCount: '拖尾采样',
290
+ instructions: '在改变速度、拖尾强度、背景、全屏模式和显示器过冲设置时,观察移动目标的后端。',
291
+ reset: '重置',
292
+ },
293
+ };
@@ -0,0 +1,9 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { monitorGhostingTest } from './entry';
3
+ export * from './entry';
4
+ export const MONITOR_GHOSTING_TEST_TOOL: ToolDefinition = {
5
+ entry: monitorGhostingTest,
6
+ Component: () => import('./component.astro'),
7
+ SEOComponent: () => import('./seo.astro'),
8
+ BibliographyComponent: () => import('./bibliography.astro'),
9
+ };
@@ -0,0 +1,195 @@
1
+ export type GhostingBackground = 'dark' | 'light' | 'grid';
2
+ export type GhostingPattern = 'bars' | 'text' | 'ufo';
3
+
4
+ interface GhostingElements {
5
+ root: HTMLElement | null;
6
+ monitor: HTMLElement | null;
7
+ track: HTMLElement | null;
8
+ target: HTMLElement | null;
9
+ speed: HTMLInputElement | null;
10
+ trail: HTMLInputElement | null;
11
+ background: HTMLSelectElement | null;
12
+ pattern: HTMLSelectElement | null;
13
+ pursuit: HTMLInputElement | null;
14
+ fullscreen: HTMLElement | null;
15
+ pause: HTMLElement | null;
16
+ reset: HTMLElement | null;
17
+ speedValue: HTMLElement | null;
18
+ trailValue: HTMLElement | null;
19
+ blur: HTMLElement | null;
20
+ frameStep: HTMLElement | null;
21
+ persistence: HTMLElement | null;
22
+ sampleCount: HTMLElement | null;
23
+ }
24
+
25
+ interface GhostingLabels {
26
+ pixelsPerSecondUnit: string;
27
+ pixelUnit: string;
28
+ millisecondUnit: string;
29
+ fullscreen: string;
30
+ exitFullscreen: string;
31
+ pause: string;
32
+ resume: string;
33
+ }
34
+
35
+ export class MonitorGhostingLab {
36
+ private speed = 960;
37
+ private trail = 5;
38
+ private background: GhostingBackground = 'dark';
39
+ private pattern: GhostingPattern = 'bars';
40
+ private pursuit = true;
41
+ private paused = false;
42
+ private resizeObserver: ResizeObserver | null = null;
43
+
44
+ constructor(
45
+ private readonly elements: GhostingElements,
46
+ private readonly labels: GhostingLabels,
47
+ ) {
48
+ this.syncFromControls();
49
+ this.bindEvents();
50
+ this.observeMotionArea();
51
+ this.render();
52
+ }
53
+
54
+ private bindEvents() {
55
+ this.bindControl(this.elements.speed, 'input');
56
+ this.bindControl(this.elements.trail, 'input');
57
+ this.bindControl(this.elements.background, 'change');
58
+ this.bindControl(this.elements.pattern, 'change');
59
+ this.bindControl(this.elements.pursuit, 'change');
60
+ this.bindClick(this.elements.fullscreen, () => this.toggleFullscreen());
61
+ this.bindClick(this.elements.pause, () => this.togglePaused());
62
+ document.addEventListener('fullscreenchange', () => this.renderFullscreenLabel());
63
+ this.bindClick(this.elements.reset, () => this.reset());
64
+ }
65
+
66
+ private updateFromControls() {
67
+ this.syncFromControls();
68
+ this.render();
69
+ }
70
+
71
+ private syncFromControls() {
72
+ this.speed = this.getInputNumber(this.elements.speed, 960);
73
+ this.trail = this.getInputNumber(this.elements.trail, 5);
74
+ this.background = this.getSelectValue(this.elements.background, 'dark');
75
+ this.pattern = this.getSelectValue(this.elements.pattern, 'bars');
76
+ this.pursuit = this.elements.pursuit?.checked === true;
77
+ }
78
+
79
+ private bindControl(element: HTMLElement | null, eventName: 'change' | 'input') {
80
+ element?.addEventListener(eventName, () => this.updateFromControls());
81
+ }
82
+
83
+ private bindClick(element: HTMLElement | null, handler: () => void) {
84
+ element?.addEventListener('click', handler);
85
+ }
86
+
87
+ private getInputNumber(element: HTMLInputElement | null, fallback: number) {
88
+ return Number(element?.value ?? fallback);
89
+ }
90
+
91
+ private getSelectValue<T extends string>(element: HTMLSelectElement | null, fallback: T) {
92
+ return (element?.value ?? fallback) as T;
93
+ }
94
+
95
+ private reset() {
96
+ if (this.elements.speed) this.elements.speed.value = '960';
97
+ if (this.elements.trail) this.elements.trail.value = '5';
98
+ if (this.elements.background) this.elements.background.value = 'dark';
99
+ if (this.elements.pattern) this.elements.pattern.value = 'bars';
100
+ if (this.elements.pursuit) this.elements.pursuit.checked = true;
101
+ this.paused = false;
102
+ this.updateFromControls();
103
+ }
104
+
105
+ private render() {
106
+ this.renderRootState();
107
+ this.renderText();
108
+ this.renderFullscreenLabel();
109
+ this.renderPauseLabel();
110
+ }
111
+
112
+ private observeMotionArea() {
113
+ if (!this.elements.track || !this.elements.target) return;
114
+
115
+ this.resizeObserver = new ResizeObserver(() => this.renderRootState());
116
+ this.resizeObserver.observe(this.elements.track);
117
+ this.resizeObserver.observe(this.elements.target);
118
+ }
119
+
120
+ private async toggleFullscreen() {
121
+ if (document.fullscreenElement) {
122
+ await document.exitFullscreen();
123
+ return;
124
+ }
125
+
126
+ await this.elements.monitor?.requestFullscreen();
127
+ }
128
+
129
+ private togglePaused() {
130
+ this.paused = !this.paused;
131
+ this.render();
132
+ }
133
+
134
+ private getEstimatedBlur() {
135
+ return Math.round((this.speed / 240) * this.trail);
136
+ }
137
+
138
+ private getTravelDuration() {
139
+ const travelDistance = this.getTravelDistance();
140
+ const duration = travelDistance / this.speed;
141
+
142
+ return Math.max(0.35, Math.min(12, duration)).toFixed(2);
143
+ }
144
+
145
+ private getTravelDistance() {
146
+ const trackWidth = this.elements.track?.clientWidth ?? 0;
147
+ const targetWidth = this.elements.target?.getBoundingClientRect().width ?? 0;
148
+
149
+ return Math.max(120, trackWidth - targetWidth);
150
+ }
151
+
152
+ private getFrameStep() {
153
+ return Math.round((this.speed / 144) * 10) / 10;
154
+ }
155
+
156
+ private getPersistence() {
157
+ return Math.round((this.trail / 10) * 16.7 * 10) / 10;
158
+ }
159
+
160
+ private getSampleCount() {
161
+ return Math.max(3, Math.round(this.trail * 2.4));
162
+ }
163
+
164
+ private renderFullscreenLabel() {
165
+ this.setText(this.elements.fullscreen, document.fullscreenElement ? this.labels.exitFullscreen : this.labels.fullscreen);
166
+ }
167
+
168
+ private renderPauseLabel() {
169
+ this.setText(this.elements.pause, this.paused ? this.labels.resume : this.labels.pause);
170
+ }
171
+
172
+ private renderText() {
173
+ this.setText(this.elements.speedValue, `${this.speed} ${this.labels.pixelsPerSecondUnit}`);
174
+ this.setText(this.elements.trailValue, String(this.trail));
175
+ this.setText(this.elements.blur, `${this.getEstimatedBlur()} ${this.labels.pixelUnit}`);
176
+ this.setText(this.elements.frameStep, `${this.getFrameStep()} ${this.labels.pixelUnit}`);
177
+ this.setText(this.elements.persistence, `${this.getPersistence()} ${this.labels.millisecondUnit}`);
178
+ this.setText(this.elements.sampleCount, String(this.getSampleCount()));
179
+ }
180
+
181
+ private setText(element: HTMLElement | null, value: string) {
182
+ if (element) element.textContent = value;
183
+ }
184
+
185
+ private renderRootState() {
186
+ if (!this.elements.root) return;
187
+
188
+ this.elements.root.style.setProperty('--mgt-duration', `${this.getTravelDuration()}s`);
189
+ this.elements.root.style.setProperty('--mgt-trail-opacity', String(this.trail / 10));
190
+ this.elements.root.dataset.background = this.background;
191
+ this.elements.root.dataset.pattern = this.pattern;
192
+ this.elements.root.dataset.pursuit = this.pursuit ? 'on' : 'off';
193
+ this.elements.root.dataset.paused = this.paused ? 'true' : 'false';
194
+ }
195
+ }