desktop-team-doc 0.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 (151) hide show
  1. package/README.md +89 -0
  2. package/content/docs/README.md +227 -0
  3. package/content/docs/index.md +352 -0
  4. package/content/docs/instructions/coding-conventions/.clang-format +65 -0
  5. package/content/docs/instructions/coding-conventions/cpp.md +132 -0
  6. package/content/docs/instructions/coding-conventions/frontend.md +612 -0
  7. package/content/docs/instructions/coding-conventions/team-wide.md +176 -0
  8. package/content/docs/instructions/workflows/assets/jira-1.png +0 -0
  9. package/content/docs/instructions/workflows/assets/jira-comment.png +0 -0
  10. package/content/docs/instructions/workflows/assets/jira-release-note.png +0 -0
  11. package/content/docs/instructions/workflows/assets/jira-tag.png +0 -0
  12. package/content/docs/instructions/workflows/code-review.md +451 -0
  13. package/content/docs/instructions/workflows/git-branch-convention.md +246 -0
  14. package/content/docs/instructions/workflows/git-commit.md +95 -0
  15. package/content/docs/instructions/workflows/jira-process.md +173 -0
  16. package/content/docs/instructions/workflows/jira-ticket-guide.md +105 -0
  17. package/content/docs/instructions/workflows/pull-request-generation.md +319 -0
  18. package/content/docs/instructions/workflows/scrum-process.md +104 -0
  19. package/content/docs/instructions/workflows/survey-project-setup.md +76 -0
  20. package/content/docs/knowledge/architecture/README.md +11 -0
  21. package/content/docs/knowledge/architecture/audio-plugin-architecture.md +213 -0
  22. package/content/docs/knowledge/architecture/cross-platform-design.md +176 -0
  23. package/content/docs/knowledge/architecture/frontend-native-bridge.md +193 -0
  24. package/content/docs/knowledge/architecture/native-command.md +189 -0
  25. package/content/docs/knowledge/architecture/state-management-architecture.md +105 -0
  26. package/content/docs/knowledge/component-library/ControlComponent/README.md +281 -0
  27. package/content/docs/knowledge/component-library/ControlComponent/accessibility/accessibility-implementation.md +503 -0
  28. package/content/docs/knowledge/component-library/ControlComponent/common-mechanisms.md +278 -0
  29. package/content/docs/knowledge/component-library/ControlComponent/core/error-handling.md +451 -0
  30. package/content/docs/knowledge/component-library/ControlComponent/core/native-interface.md +515 -0
  31. package/content/docs/knowledge/component-library/ControlComponent/core/state-management.md +509 -0
  32. package/content/docs/knowledge/component-library/ControlComponent/creating-new-controls.md +654 -0
  33. package/content/docs/knowledge/component-library/ControlComponent/design/api-design-reference.md +1142 -0
  34. package/content/docs/knowledge/component-library/ControlComponent/design/design-principles.md +336 -0
  35. package/content/docs/knowledge/component-library/ControlComponent/design/styling-architecture.md +595 -0
  36. package/content/docs/knowledge/component-library/ControlComponent/design/visual-feedback.md +456 -0
  37. package/content/docs/knowledge/component-library/ControlComponent/development-environment.md +213 -0
  38. package/content/docs/knowledge/component-library/ControlComponent/interaction/gesture-algorithms.md +705 -0
  39. package/content/docs/knowledge/component-library/ControlComponent/interaction/touch-support.md +525 -0
  40. package/content/docs/knowledge/component-library/ControlComponent/interaction/value-processing-patterns.md +801 -0
  41. package/content/docs/knowledge/component-library/ControlComponent/interaction/velocity-damping-systems.md +741 -0
  42. package/content/docs/knowledge/component-library/ControlComponent/knob/architecture.md +490 -0
  43. package/content/docs/knowledge/component-library/ControlComponent/knob/how-to-use.md +304 -0
  44. package/content/docs/knowledge/component-library/ControlComponent/knob/index.md +105 -0
  45. package/content/docs/knowledge/component-library/ControlComponent/optimization/performance-benchmarks.md +535 -0
  46. package/content/docs/knowledge/component-library/ControlComponent/optimization/performance-optimization.md +1092 -0
  47. package/content/docs/knowledge/component-library/ControlComponent/quick-start.md +345 -0
  48. package/content/docs/knowledge/component-library/ControlComponent/slider/architecture.md +444 -0
  49. package/content/docs/knowledge/component-library/ControlComponent/slider/how-to-use.md +470 -0
  50. package/content/docs/knowledge/component-library/ControlComponent/slider/index.md +107 -0
  51. package/content/docs/knowledge/component-library/ControlComponent/testing-guide.md +950 -0
  52. package/content/docs/knowledge/component-library/ControlComponent/troubleshooting.md +657 -0
  53. package/content/docs/knowledge/component-library/frontend-develop/LICENSE.txt +176 -0
  54. package/content/docs/knowledge/component-library/frontend-develop/SKILL.md +124 -0
  55. package/content/docs/knowledge/component-library/frontend-develop/references/code-organization.md +620 -0
  56. package/content/docs/knowledge/component-library/frontend-develop/references/coding-standards.md +275 -0
  57. package/content/docs/knowledge/component-library/frontend-develop/references/component-reusability.md +559 -0
  58. package/content/docs/knowledge/component-library/frontend-develop/references/examples.md +554 -0
  59. package/content/docs/knowledge/component-library/frontend-develop/references/layout-separation.md +638 -0
  60. package/content/docs/knowledge/component-library/frontend-develop/references/performance-optimization.md +678 -0
  61. package/content/docs/knowledge/component-library/frontend-develop/references/state-management.md +331 -0
  62. package/content/docs/knowledge/component-library/frontend-develop/references/styling-guidelines.md +349 -0
  63. package/content/docs/knowledge/component-library/frontend-develop/references/type-safety.md +493 -0
  64. package/content/docs/knowledge/development/assets/cyberduck-aws-credentials.png +0 -0
  65. package/content/docs/knowledge/development/assets/postman-environment-setup.png +0 -0
  66. package/content/docs/knowledge/development/aws-storage.md +95 -0
  67. package/content/docs/knowledge/development/crm-system.md +22 -0
  68. package/content/docs/knowledge/development/glossary.md +246 -0
  69. package/content/docs/knowledge/development/pg-api-guide.md +71 -0
  70. package/content/docs/knowledge/development/staging-license-management.md +44 -0
  71. package/content/docs/knowledge/development/tech-stack.md +240 -0
  72. package/content/docs/knowledge/domain/popup-system.md +106 -0
  73. package/content/docs/knowledge/domain/sigpath.md +264 -0
  74. package/content/docs/knowledge/environment-setup/aax-signing-update.md +149 -0
  75. package/content/docs/knowledge/environment-setup/assets/aax-1.png +0 -0
  76. package/content/docs/knowledge/environment-setup/assets/aax-2.png +0 -0
  77. package/content/docs/knowledge/environment-setup/assets/aax-3.png +0 -0
  78. package/content/docs/knowledge/environment-setup/assets/aax-4.png +0 -0
  79. package/content/docs/knowledge/environment-setup/assets/aax-5.png +0 -0
  80. package/content/docs/knowledge/environment-setup/assets/aax-6.png +0 -0
  81. package/content/docs/knowledge/environment-setup/assets/aax-7.png +0 -0
  82. package/content/docs/knowledge/environment-setup/assets/buildmachine-1.png +0 -0
  83. package/content/docs/knowledge/environment-setup/assets/buildmachine-10.png +0 -0
  84. package/content/docs/knowledge/environment-setup/assets/buildmachine-11.png +0 -0
  85. package/content/docs/knowledge/environment-setup/assets/buildmachine-12.png +0 -0
  86. package/content/docs/knowledge/environment-setup/assets/buildmachine-13.png +0 -0
  87. package/content/docs/knowledge/environment-setup/assets/buildmachine-14.png +0 -0
  88. package/content/docs/knowledge/environment-setup/assets/buildmachine-2.png +0 -0
  89. package/content/docs/knowledge/environment-setup/assets/buildmachine-3.png +0 -0
  90. package/content/docs/knowledge/environment-setup/assets/buildmachine-4.png +0 -0
  91. package/content/docs/knowledge/environment-setup/assets/buildmachine-5.png +0 -0
  92. package/content/docs/knowledge/environment-setup/assets/buildmachine-6.png +0 -0
  93. package/content/docs/knowledge/environment-setup/assets/buildmachine-7.png +0 -0
  94. package/content/docs/knowledge/environment-setup/assets/buildmachine-8.png +0 -0
  95. package/content/docs/knowledge/environment-setup/assets/buildmachine-9.png +0 -0
  96. package/content/docs/knowledge/environment-setup/build-machine-setup.md +224 -0
  97. package/content/docs/knowledge/environment-setup/build-machine-troubleshooting.md +193 -0
  98. package/content/docs/knowledge/implementation-guides/adding-amp.md +190 -0
  99. package/content/docs/knowledge/implementation-guides/adding-fx.md +111 -0
  100. package/content/docs/knowledge/implementation-guides/cab-integration.md +194 -0
  101. package/content/docs/knowledge/implementation-guides/custom-pedal-integration.md +309 -0
  102. package/content/docs/knowledge/projects/BIAS_ONE_GUI/README.md +17 -0
  103. package/content/manifest.json +122 -0
  104. package/content/rules/cpp.mdc +135 -0
  105. package/content/rules/frontend.mdc +615 -0
  106. package/content/rules/index.mdc +256 -0
  107. package/content/rules/knowledge.mdc +46 -0
  108. package/content/rules/team-wide.mdc +179 -0
  109. package/content/rules/workflows.mdc +43 -0
  110. package/content/tools/agents/context-compressor.md +357 -0
  111. package/content/tools/agents/context-writer.md +328 -0
  112. package/content/tools/agents/release-notes-generator.md +389 -0
  113. package/content/tools/agents/srs-writer-agent.md +63 -0
  114. package/content/tools/mcp/README.md +25 -0
  115. package/content/tools/mcp/mcp-desktop-team.example.json +13 -0
  116. package/content/tools/skills/frontend-develop/LICENSE.txt +176 -0
  117. package/content/tools/skills/frontend-develop/SKILL.md +124 -0
  118. package/content/tools/skills/frontend-develop/references/code-organization.md +620 -0
  119. package/content/tools/skills/frontend-develop/references/coding-standards.md +275 -0
  120. package/content/tools/skills/frontend-develop/references/component-reusability.md +559 -0
  121. package/content/tools/skills/frontend-develop/references/examples.md +554 -0
  122. package/content/tools/skills/frontend-develop/references/layout-separation.md +638 -0
  123. package/content/tools/skills/frontend-develop/references/performance-optimization.md +678 -0
  124. package/content/tools/skills/frontend-develop/references/state-management.md +331 -0
  125. package/content/tools/skills/frontend-develop/references/styling-guidelines.md +349 -0
  126. package/content/tools/skills/frontend-develop/references/type-safety.md +493 -0
  127. package/content/tools/slash-commands/commit.md +17 -0
  128. package/content/tools/slash-commands/context-compress.md +149 -0
  129. package/content/tools/slash-commands/context-write.md +92 -0
  130. package/content/tools/slash-commands/jira.md +12 -0
  131. package/content/tools/slash-commands/pr-gen.md +12 -0
  132. package/content/tools/slash-commands/pr-review.md +12 -0
  133. package/dist/commands/detect.d.ts +1 -0
  134. package/dist/commands/detect.js +33 -0
  135. package/dist/commands/install.d.ts +1 -0
  136. package/dist/commands/install.js +100 -0
  137. package/dist/commands/uninstall.d.ts +1 -0
  138. package/dist/commands/uninstall.js +132 -0
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.js +53 -0
  141. package/dist/lib/detect-env.d.ts +3 -0
  142. package/dist/lib/detect-env.js +52 -0
  143. package/dist/lib/prompt-env.d.ts +3 -0
  144. package/dist/lib/prompt-env.js +16 -0
  145. package/dist/lib/resolve-doc-repo.d.ts +14 -0
  146. package/dist/lib/resolve-doc-repo.js +61 -0
  147. package/dist/lib/symlink.d.ts +7 -0
  148. package/dist/lib/symlink.js +60 -0
  149. package/dist/lib/sync-from-manifest.d.ts +8 -0
  150. package/dist/lib/sync-from-manifest.js +64 -0
  151. package/package.json +46 -0
@@ -0,0 +1,705 @@
1
+ # 手勢處理演算法
2
+
3
+ 本文檔詳細說明音訊控制項中的手勢處理演算法,包括事件捕獲、座標轉換、精確模式控制等核心實作。
4
+
5
+ ---
6
+
7
+ ## 1. 基礎拖曳手勢模式
8
+
9
+ ### Pointer Events API
10
+
11
+ 現代瀏覽器推薦使用 Pointer Events API,它統一處理滑鼠、觸控和觸控筆輸入:
12
+
13
+ ```typescript
14
+ interface PointerEventHandlers {
15
+ onPointerDown: (event: React.PointerEvent) => void;
16
+ onPointerMove: (event: React.PointerEvent) => void;
17
+ onPointerUp: (event: React.PointerEvent) => void;
18
+ onPointerCancel: (event: React.PointerEvent) => void;
19
+ }
20
+ ```
21
+
22
+ ### 事件捕獲模式
23
+
24
+ 關鍵是使用 `setPointerCapture` 確保拖曳時游標離開元件也能持續追蹤:
25
+
26
+ ```typescript
27
+ /**
28
+ * 拖曳事件管理器
29
+ * 確保拖曳過程中游標離開元件也能持續追蹤
30
+ */
31
+ class DragEventManager {
32
+ private isDragging = false;
33
+ private startPosition: Point = { x: 0, y: 0 };
34
+ private currentPosition: Point = { x: 0, y: 0 };
35
+
36
+ handlePointerDown(event: PointerEvent, element: HTMLElement): void {
37
+ this.isDragging = true;
38
+ this.startPosition = { x: event.clientX, y: event.clientY };
39
+ this.currentPosition = { ...this.startPosition };
40
+
41
+ // 關鍵:捕獲指標,確保事件持續傳遞到此元素
42
+ element.setPointerCapture(event.pointerId);
43
+
44
+ // 防止文字選取和預設行為
45
+ event.preventDefault();
46
+ }
47
+
48
+ handlePointerMove(event: PointerEvent): Point | null {
49
+ if (!this.isDragging) return null;
50
+
51
+ const previousPosition = { ...this.currentPosition };
52
+ this.currentPosition = { x: event.clientX, y: event.clientY };
53
+
54
+ // 返回相對移動量
55
+ return {
56
+ x: this.currentPosition.x - previousPosition.x,
57
+ y: this.currentPosition.y - previousPosition.y,
58
+ };
59
+ }
60
+
61
+ handlePointerUp(event: PointerEvent, element: HTMLElement): void {
62
+ if (!this.isDragging) return;
63
+
64
+ this.isDragging = false;
65
+ element.releasePointerCapture(event.pointerId);
66
+ }
67
+
68
+ getTotalMovement(): Point {
69
+ return {
70
+ x: this.currentPosition.x - this.startPosition.x,
71
+ y: this.currentPosition.y - this.startPosition.y,
72
+ };
73
+ }
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## 2. movementX/movementY 與跨平台問題
80
+
81
+ ### MDN 警告
82
+
83
+ `MouseEvent.movementX` 和 `MouseEvent.movementY` 是瀏覽器提供的相對移動量,但存在跨平台不一致問題:
84
+
85
+ > "瀏覽器對 movementX 和 screenX 使用的單位與規範定義的不同。根據瀏覽器和作業系統的差異,movementX 的單位可能是物理像素、邏輯像素或 CSS 像素。"
86
+
87
+ ### 解決方案:sensitivity 參數
88
+
89
+ ```typescript
90
+ /**
91
+ * 跨平台的移動量標準化
92
+ */
93
+ function normalizeMovement(
94
+ event: PointerEvent,
95
+ sensitivity: number,
96
+ platformAdapter: PlatformAdapter
97
+ ): Point {
98
+ // 方法 1: 使用 movementX/Y (簡單但有跨平台問題)
99
+ const rawMovement = {
100
+ x: event.movementX ?? 0,
101
+ y: event.movementY ?? 0,
102
+ };
103
+
104
+ // 方法 2: 手動計算差值 (更可靠)
105
+ // const rawMovement = calculateDelta(event, lastPosition);
106
+
107
+ // 應用平台校正因子
108
+ const platformFactor = platformAdapter.getMovementFactor();
109
+
110
+ return {
111
+ x: rawMovement.x * sensitivity * platformFactor,
112
+ y: rawMovement.y * sensitivity * platformFactor,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * 平台適配器
118
+ */
119
+ class PlatformAdapter {
120
+ private readonly isMac: boolean;
121
+ private readonly isHighDPI: boolean;
122
+
123
+ constructor() {
124
+ this.isMac = navigator.platform.toLowerCase().includes('mac');
125
+ this.isHighDPI = window.devicePixelRatio > 1;
126
+ }
127
+
128
+ getMovementFactor(): number {
129
+ // macOS 的觸控板通常需要較低的靈敏度
130
+ if (this.isMac) {
131
+ return 0.8;
132
+ }
133
+
134
+ // 高 DPI 螢幕可能需要調整
135
+ if (this.isHighDPI) {
136
+ return 1 / window.devicePixelRatio;
137
+ }
138
+
139
+ return 1.0;
140
+ }
141
+
142
+ getPreciseModifierKey(): 'metaKey' | 'ctrlKey' {
143
+ return this.isMac ? 'metaKey' : 'ctrlKey';
144
+ }
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 3. GestureToValueConverter 完整實作
151
+
152
+ ### 統一的手勢轉數值轉換器
153
+
154
+ ```typescript
155
+ /**
156
+ * 手勢配置
157
+ */
158
+ interface GestureConfig {
159
+ mode: AudioControlMode;
160
+ sensitivity: number;
161
+ damping?: DampingConfig;
162
+ velocity?: VelocityConfig;
163
+ rotaryMethod?: 'simple' | 'weighted' | 'angular';
164
+ }
165
+
166
+ /**
167
+ * 手勢數據
168
+ */
169
+ interface GestureData {
170
+ movement: [number, number]; // [mx, my]
171
+ event: PointerEvent;
172
+ startPosition?: Point;
173
+ currentPosition?: Point;
174
+ centerPosition?: Point; // 用於圓形模式
175
+ }
176
+
177
+ /**
178
+ * 轉換上下文
179
+ */
180
+ interface ConversionContext {
181
+ velocityTracker: VelocityTracker;
182
+ preciseState: PreciseState;
183
+ min: number;
184
+ max: number;
185
+ }
186
+
187
+ /**
188
+ * 統一的手勢轉數值轉換器
189
+ */
190
+ class GestureToValueConverter {
191
+ private config: GestureConfig;
192
+
193
+ constructor(config: GestureConfig) {
194
+ this.config = config;
195
+ }
196
+
197
+ /**
198
+ * 主要轉換函式
199
+ */
200
+ convert(gesture: GestureData, context: ConversionContext): number {
201
+ // 1. 計算基礎移動增量
202
+ let delta = this.calculateBaseDelta(gesture);
203
+
204
+ // 2. 應用靈敏度
205
+ delta *= this.config.sensitivity;
206
+
207
+ // 3. 應用修飾鍵效果 (Damping)
208
+ delta = this.applyModifiers(delta, gesture.event, context);
209
+
210
+ // 4. 應用速度感應
211
+ if (this.config.velocity?.enabled) {
212
+ delta = this.applyVelocityScaling(delta, context.velocityTracker);
213
+ }
214
+
215
+ // 5. 轉換為數值變化量 (基於 200px = 全範圍的假設)
216
+ const range = context.max - context.min;
217
+ return (delta / 200) * range;
218
+ }
219
+
220
+ /**
221
+ * 計算基礎移動增量
222
+ */
223
+ private calculateBaseDelta(gesture: GestureData): number {
224
+ const [mx, my] = gesture.movement;
225
+
226
+ switch (this.config.mode) {
227
+ case 'linear-horizontal':
228
+ return mx;
229
+
230
+ case 'linear-vertical':
231
+ return -my;
232
+
233
+ case 'rotary':
234
+ return this.calculateRotaryDelta(mx, my);
235
+
236
+ case 'rotary-circular':
237
+ return this.calculateCircularDelta(gesture);
238
+
239
+ default:
240
+ return 0;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 旋鈕拖曳增量計算 - 三種方法
246
+ */
247
+ private calculateRotaryDelta(mx: number, my: number): number {
248
+ switch (this.config.rotaryMethod ?? 'simple') {
249
+ case 'simple':
250
+ // 方法1:簡單加權組合 (react-knob-headless 風格)
251
+ // 上或右為正,下或左為負
252
+ return mx - my;
253
+
254
+ case 'weighted':
255
+ // 方法2:向量長度考慮 (更精確)
256
+ const magnitude = Math.sqrt(mx * mx + my * my);
257
+ const direction = Math.sign(mx - my);
258
+ return magnitude * direction * 0.7;
259
+
260
+ case 'angular':
261
+ // 方法3:角度感知組合 (45度方向最敏感)
262
+ const angle = Math.atan2(my, mx);
263
+ const angleFactor = Math.cos(angle - Math.PI / 4);
264
+ return (mx - my) * Math.abs(angleFactor);
265
+
266
+ default:
267
+ return mx - my;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * 圓形路徑拖曳增量計算
273
+ */
274
+ private calculateCircularDelta(gesture: GestureData): number {
275
+ const { startPosition, currentPosition, centerPosition } = gesture;
276
+
277
+ if (!startPosition || !currentPosition || !centerPosition) {
278
+ return 0;
279
+ }
280
+
281
+ // 計算角度變化
282
+ const startAngle = Math.atan2(
283
+ startPosition.y - centerPosition.y,
284
+ startPosition.x - centerPosition.x
285
+ );
286
+
287
+ const currentAngle = Math.atan2(
288
+ currentPosition.y - centerPosition.y,
289
+ currentPosition.x - centerPosition.x
290
+ );
291
+
292
+ // 處理角度跨越 (-π 到 π)
293
+ let angleDelta = currentAngle - startAngle;
294
+
295
+ if (angleDelta > Math.PI) {
296
+ angleDelta -= 2 * Math.PI;
297
+ } else if (angleDelta < -Math.PI) {
298
+ angleDelta += 2 * Math.PI;
299
+ }
300
+
301
+ // 轉換為數值變化 (270° = 全範圍)
302
+ const fullRotation = (270 * Math.PI) / 180;
303
+ return (angleDelta / fullRotation) * 200; // 乘以 200 統一到像素單位
304
+ }
305
+
306
+ /**
307
+ * 應用修飾鍵效果
308
+ */
309
+ private applyModifiers(
310
+ delta: number,
311
+ event: PointerEvent,
312
+ context: ConversionContext
313
+ ): number {
314
+ const { damping } = this.config;
315
+
316
+ if (!damping?.enabled || !damping?.key) {
317
+ return delta;
318
+ }
319
+
320
+ const keyMap: Record<string, keyof PointerEvent> = {
321
+ shift: 'shiftKey',
322
+ ctrl: 'ctrlKey',
323
+ alt: 'altKey',
324
+ meta: 'metaKey',
325
+ };
326
+
327
+ const isDamped = Boolean(event[keyMap[damping.key]]);
328
+
329
+ if (isDamped) {
330
+ return delta * (damping.factor ?? 0.1);
331
+ }
332
+
333
+ return delta;
334
+ }
335
+
336
+ /**
337
+ * 應用速度感應縮放
338
+ */
339
+ private applyVelocityScaling(
340
+ delta: number,
341
+ velocityTracker: VelocityTracker
342
+ ): number {
343
+ const velocity = velocityTracker.getSmoothedVelocity();
344
+ const config = this.config.velocity!;
345
+
346
+ const threshold = config.threshold ?? 0.5;
347
+ if (velocity < threshold) {
348
+ return delta;
349
+ }
350
+
351
+ const normalizedVelocity = velocity - threshold;
352
+ const sensitivity = config.sensitivity ?? 1;
353
+
354
+ let multiplier: number;
355
+
356
+ switch (config.curve) {
357
+ case 'exponential':
358
+ multiplier = 1 + Math.pow(normalizedVelocity * sensitivity, 2);
359
+ break;
360
+ case 'logarithmic':
361
+ multiplier = 1 + Math.log(1 + normalizedVelocity * sensitivity);
362
+ break;
363
+ case 'linear':
364
+ default:
365
+ multiplier = 1 + normalizedVelocity * sensitivity;
366
+ }
367
+
368
+ const maxMultiplier = config.maxMultiplier ?? 10;
369
+ return delta * Math.min(multiplier, maxMultiplier);
370
+ }
371
+ }
372
+ ```
373
+
374
+ ---
375
+
376
+ ## 4. PreciseModeController 完整實作
377
+
378
+ ### 距離解鎖的精確模式
379
+
380
+ 基於 react-rotary-knob 的設計,防止意外跳躍並提供漸進式精確度:
381
+
382
+ ```typescript
383
+ /**
384
+ * 精確模式配置
385
+ */
386
+ interface PreciseModeConfig {
387
+ enabled?: boolean;
388
+ unlockDistance?: number; // 解鎖距離 (px),預設 50
389
+ basePrecision?: number; // 基礎精確度,預設 1.0
390
+ precisionGain?: number; // 精確度增益,預設 0.01
391
+ minPrecision?: number; // 最小精確度,預設 0.1
392
+ }
393
+
394
+ /**
395
+ * 精確模式狀態
396
+ */
397
+ interface PreciseState {
398
+ isUnlocked: boolean;
399
+ startPosition: Point | null;
400
+ accumulatedDistance: number;
401
+ precisionLevel: number;
402
+ }
403
+
404
+ /**
405
+ * 精確度計算結果
406
+ */
407
+ interface PrecisionResult {
408
+ isActive: boolean;
409
+ multiplier: number;
410
+ }
411
+
412
+ /**
413
+ * 精確模式控制器
414
+ * 防止意外跳躍,提供漸進式精確度
415
+ */
416
+ class PreciseModeController {
417
+ private config: PreciseModeConfig;
418
+ private state: PreciseState;
419
+
420
+ constructor(config: PreciseModeConfig = {}) {
421
+ this.config = {
422
+ enabled: true,
423
+ unlockDistance: 50,
424
+ basePrecision: 1.0,
425
+ precisionGain: 0.01,
426
+ minPrecision: 0.1,
427
+ ...config,
428
+ };
429
+
430
+ this.state = this.createInitialState();
431
+ }
432
+
433
+ private createInitialState(): PreciseState {
434
+ return {
435
+ isUnlocked: false,
436
+ startPosition: null,
437
+ accumulatedDistance: 0,
438
+ precisionLevel: 1.0,
439
+ };
440
+ }
441
+
442
+ /**
443
+ * 重置狀態 (拖曳結束時調用)
444
+ */
445
+ reset(): void {
446
+ this.state = this.createInitialState();
447
+ }
448
+
449
+ /**
450
+ * 處理指標移動
451
+ * @param currentPosition 當前指標位置
452
+ * @param isFirst 是否為拖曳開始
453
+ */
454
+ process(currentPosition: Point, isFirst: boolean): PrecisionResult {
455
+ if (!this.config.enabled) {
456
+ return { isActive: false, multiplier: 1.0 };
457
+ }
458
+
459
+ if (isFirst) {
460
+ this.state.startPosition = currentPosition;
461
+ this.state.isUnlocked = false;
462
+ this.state.accumulatedDistance = 0;
463
+ }
464
+
465
+ if (!this.state.startPosition) {
466
+ return { isActive: false, multiplier: 1.0 };
467
+ }
468
+
469
+ // 計算累積距離
470
+ this.state.accumulatedDistance = this.calculateDistance(
471
+ this.state.startPosition,
472
+ currentPosition
473
+ );
474
+
475
+ // 檢查解鎖條件
476
+ if (!this.state.isUnlocked) {
477
+ if (this.state.accumulatedDistance >= this.config.unlockDistance!) {
478
+ this.state.isUnlocked = true;
479
+ } else {
480
+ // 尚未解鎖,返回零變化 (防止意外跳躍)
481
+ return { isActive: false, multiplier: 0 };
482
+ }
483
+ }
484
+
485
+ // 計算精確度乘數
486
+ this.state.precisionLevel = this.calculatePrecisionLevel(
487
+ this.state.accumulatedDistance
488
+ );
489
+
490
+ return {
491
+ isActive: true,
492
+ multiplier: this.state.precisionLevel,
493
+ };
494
+ }
495
+
496
+ /**
497
+ * 計算精確度等級
498
+ * 距離越大,精確度越高 (乘數越小)
499
+ */
500
+ private calculatePrecisionLevel(distance: number): number {
501
+ const { basePrecision, precisionGain, minPrecision } = this.config;
502
+
503
+ // 對數曲線:初期快速提升精確度,後期趨緩
504
+ const precision = basePrecision! / (1 + distance * precisionGain!);
505
+
506
+ return Math.max(minPrecision!, precision);
507
+ }
508
+
509
+ /**
510
+ * 計算兩點間距離
511
+ */
512
+ private calculateDistance(p1: Point, p2: Point): number {
513
+ const dx = p2.x - p1.x;
514
+ const dy = p2.y - p1.y;
515
+ return Math.sqrt(dx * dx + dy * dy);
516
+ }
517
+
518
+ /**
519
+ * 獲取當前狀態 (用於調試或 UI 顯示)
520
+ */
521
+ getState(): Readonly<PreciseState> {
522
+ return { ...this.state };
523
+ }
524
+ }
525
+ ```
526
+
527
+ ---
528
+
529
+ ## 5. Pointer Lock API (無界拖曳)
530
+
531
+ ### 問題背景
532
+
533
+ 當滑鼠拖曳到螢幕邊緣時,無法再提供進一步的運動增量,互動被迫中斷。
534
+
535
+ ### 解決方案
536
+
537
+ ```typescript
538
+ /**
539
+ * 指標鎖定管理器
540
+ * 實現無界拖曳體驗
541
+ */
542
+ class PointerLockManager {
543
+ private isLocked = false;
544
+ private element: HTMLElement | null = null;
545
+ private onMovement: ((mx: number, my: number) => void) | null = null;
546
+
547
+ /**
548
+ * 請求指標鎖定
549
+ */
550
+ async requestLock(
551
+ element: HTMLElement,
552
+ onMovement: (mx: number, my: number) => void
553
+ ): Promise<boolean> {
554
+ this.element = element;
555
+ this.onMovement = onMovement;
556
+
557
+ try {
558
+ await element.requestPointerLock();
559
+ this.isLocked = true;
560
+ this.attachListeners();
561
+ return true;
562
+ } catch (error) {
563
+ console.warn('Pointer Lock not supported or denied:', error);
564
+ return false;
565
+ }
566
+ }
567
+
568
+ /**
569
+ * 釋放指標鎖定
570
+ */
571
+ exitLock(): void {
572
+ if (this.isLocked) {
573
+ document.exitPointerLock();
574
+ this.isLocked = false;
575
+ this.removeListeners();
576
+ }
577
+ }
578
+
579
+ private attachListeners(): void {
580
+ document.addEventListener('mousemove', this.handleMouseMove);
581
+ document.addEventListener('pointerlockchange', this.handleLockChange);
582
+ document.addEventListener('pointerlockerror', this.handleLockError);
583
+ }
584
+
585
+ private removeListeners(): void {
586
+ document.removeEventListener('mousemove', this.handleMouseMove);
587
+ document.removeEventListener('pointerlockchange', this.handleLockChange);
588
+ document.removeEventListener('pointerlockerror', this.handleLockError);
589
+ }
590
+
591
+ private handleMouseMove = (event: MouseEvent): void => {
592
+ if (this.isLocked && this.onMovement) {
593
+ // 在鎖定模式下,movementX/Y 持續提供相對運動
594
+ this.onMovement(event.movementX, event.movementY);
595
+ }
596
+ };
597
+
598
+ private handleLockChange = (): void => {
599
+ if (document.pointerLockElement !== this.element) {
600
+ this.isLocked = false;
601
+ this.removeListeners();
602
+ }
603
+ };
604
+
605
+ private handleLockError = (): void => {
606
+ console.error('Pointer Lock error');
607
+ this.isLocked = false;
608
+ };
609
+ }
610
+
611
+ /**
612
+ * 整合到 Hook 中的使用方式
613
+ */
614
+ function useAudioControlWithPointerLock(options: UseAudioControlOptions) {
615
+ const { usePointerLock = false } = options;
616
+ const pointerLockManager = useRef(new PointerLockManager());
617
+
618
+ const handlePointerDown = useCallback((e: React.PointerEvent) => {
619
+ // 標準處理...
620
+
621
+ if (usePointerLock) {
622
+ pointerLockManager.current.requestLock(
623
+ e.currentTarget as HTMLElement,
624
+ (mx, my) => {
625
+ // 在鎖定模式下處理移動
626
+ handleMovement(mx, my);
627
+ }
628
+ );
629
+ }
630
+ }, [usePointerLock]);
631
+
632
+ const handlePointerUp = useCallback((e: React.PointerEvent) => {
633
+ // 標準處理...
634
+
635
+ if (usePointerLock) {
636
+ pointerLockManager.current.exitLock();
637
+ }
638
+ }, [usePointerLock]);
639
+
640
+ // ...
641
+ }
642
+ ```
643
+
644
+ ---
645
+
646
+ ## 6. RxJS 拖曳處理 (替代方案)
647
+
648
+ 如果專案使用 RxJS,以下是一個使用 RxJS 處理滑鼠事件的替代方案:
649
+
650
+ ```typescript
651
+ // RxJS 拖曳處理模式
652
+ const useMouseMoveRx = (
653
+ onMove: (delta: number) => void,
654
+ sensitivity: number
655
+ ) => {
656
+ useEffect(() => {
657
+ const mouseMove$ = fromEvent<MouseEvent>(document, 'mousemove');
658
+ const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup');
659
+
660
+ const subscription = mouseMove$.pipe(
661
+ takeUntil(mouseUp$),
662
+ map(event => -event.movementY * sensitivity),
663
+ throttleTime(16) // 60fps
664
+ ).subscribe(onMove);
665
+
666
+ return () => subscription.unsubscribe();
667
+ }, [onMove, sensitivity]);
668
+ };
669
+ ```
670
+
671
+ ### 實作選項對照
672
+
673
+ | 面向 | 原生實作 | RxJS 實作 |
674
+ |------|---------|---------|
675
+ | 事件 API | Pointer Events | fromEvent + mousemove |
676
+ | 串流處理 | useCallback + RAF | pipe + throttleTime |
677
+ | 取消訂閱 | removeEventListener | subscription.unsubscribe |
678
+ | 學習曲線 | 較低 | 需要 RxJS 知識 |
679
+
680
+ ---
681
+
682
+ ## 總結
683
+
684
+ ### 演算法選擇指南
685
+
686
+ | 場景 | 推薦演算法 | 原因 |
687
+ |------|-----------|------|
688
+ | 一般旋鈕 | `rotary (simple)` | 最大容錯性,mx - my |
689
+ | 專業混音台 | `rotary (weighted)` | 更精確的向量計算 |
690
+ | 圓形控制項 | `rotary-circular` | 真實的角度追蹤 |
691
+ | 大範圍參數 | `+ Pointer Lock` | 無界拖曳體驗 |
692
+ | 精細調整 | `+ PreciseModeController` | 防跳躍 + 漸進精確度 |
693
+
694
+ ### 核心公式速查
695
+
696
+ ```typescript
697
+ // 基礎轉換
698
+ delta = calculateBaseDelta(movement, mode);
699
+
700
+ // 完整轉換 (含所有修飾)
701
+ valueChange = delta * sensitivity * dampingMultiplier * velocityMultiplier * precisionMultiplier;
702
+
703
+ // 範圍映射 (200px = 全範圍)
704
+ finalChange = (valueChange / 200) * (max - min);
705
+ ```