blacktrigram 0.7.39 → 0.7.41

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 (354) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioAssetLoader.js.map +1 -1
  3. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  4. package/lib/audio/AudioCache.js.map +1 -1
  5. package/lib/audio/AudioManager.js.map +1 -1
  6. package/lib/audio/AudioMonitor.js.map +1 -1
  7. package/lib/audio/AudioPool.js.map +1 -1
  8. package/lib/audio/AudioProvider.js.map +1 -1
  9. package/lib/audio/AudioUtils.js.map +1 -1
  10. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  11. package/lib/audio/VariantSelector.js.map +1 -1
  12. package/lib/audio/types.js.map +1 -1
  13. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  14. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  15. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  18. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  19. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  20. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  25. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  26. package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
  27. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  28. package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
  29. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  30. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  31. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  32. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  33. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  34. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  36. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  37. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  39. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  42. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  44. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  45. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  46. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  47. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  48. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  49. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  50. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  51. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  52. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  53. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  54. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  55. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  56. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  57. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  60. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  61. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  62. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  63. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  64. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  65. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  66. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  67. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  72. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  74. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  75. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  76. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  77. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  78. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  84. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  85. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  86. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  87. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  88. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  90. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  91. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  93. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  94. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  96. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  97. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  98. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  99. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  100. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  101. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  102. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  103. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  104. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  105. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  106. package/lib/components/shared/base/BaseButton.js.map +1 -1
  107. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  108. package/lib/components/shared/base/BasePanel.js.map +1 -1
  109. package/lib/components/shared/base/BaseText.js.map +1 -1
  110. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  111. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  112. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  113. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  114. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  115. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  116. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  117. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  118. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  122. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  123. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  124. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  125. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  126. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  127. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  128. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  129. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  131. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  134. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  135. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  136. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  137. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  138. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  139. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  140. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  141. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  142. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  143. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  144. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  145. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  146. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  147. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  149. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  150. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  151. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  152. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  153. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  154. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  155. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  157. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  158. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  159. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  160. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  161. package/lib/components/shared/ui/BackButton.js.map +1 -1
  162. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  163. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  164. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  165. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  166. package/lib/components/shared/ui/SplashScreen.js +2 -2
  167. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  168. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  169. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  170. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  171. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  172. package/lib/constants/bodyDimensions.js.map +1 -1
  173. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  174. package/lib/data/archetypeClothing.js.map +1 -1
  175. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  176. package/lib/data/techniqueMappings.js.map +1 -1
  177. package/lib/data/techniques.js.map +1 -1
  178. package/lib/hooks/useActionFeedback.js.map +1 -1
  179. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  180. package/lib/hooks/useCombatTimer.js.map +1 -1
  181. package/lib/hooks/useDebounce.js.map +1 -1
  182. package/lib/hooks/useHUDLayout.js.map +1 -1
  183. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  184. package/lib/hooks/useKeyboardControls.js.map +1 -1
  185. package/lib/hooks/useMatchCountdown.js.map +1 -1
  186. package/lib/hooks/useMuscleActivation.js.map +1 -1
  187. package/lib/hooks/usePauseMenu.js.map +1 -1
  188. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  189. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  190. package/lib/hooks/useRoundTransition.js.map +1 -1
  191. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  192. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  193. package/lib/hooks/useThrottle.js.map +1 -1
  194. package/lib/hooks/useTouchControls.js.map +1 -1
  195. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  196. package/lib/hooks/useWindowSize.js.map +1 -1
  197. package/lib/systems/CombatSystem.js.map +1 -1
  198. package/lib/systems/EffectCalculator.js.map +1 -1
  199. package/lib/systems/LayoutSystem.js.map +1 -1
  200. package/lib/systems/PlayerEffectManager.js.map +1 -1
  201. package/lib/systems/ResponsiveScaling.js.map +1 -1
  202. package/lib/systems/TrigramSystem.js.map +1 -1
  203. package/lib/systems/VitalPointSystem.js.map +1 -1
  204. package/lib/systems/ai/AIPersonality.js.map +1 -1
  205. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  206. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  207. package/lib/systems/ai/ComboSystem.js.map +1 -1
  208. package/lib/systems/ai/DecisionTree.js.map +1 -1
  209. package/lib/systems/ai/TrainingAI.js.map +1 -1
  210. package/lib/systems/ai/types.js.map +1 -1
  211. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  212. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  213. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  214. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  215. package/lib/systems/animation/builders/KeyframeInterpolation.js +3 -90
  216. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  217. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  218. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  219. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  220. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  221. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  222. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  223. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  224. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  225. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  226. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  227. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  228. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  229. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  230. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  231. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  232. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  233. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  234. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  235. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  236. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  237. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  238. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  239. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  240. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  241. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  242. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  243. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  244. package/lib/systems/animation/core/types.js.map +1 -1
  245. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  246. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  247. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  248. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  249. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  250. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  251. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  252. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  253. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  254. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  255. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  256. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  257. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  258. package/lib/systems/bodypart/types.js.map +1 -1
  259. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  260. package/lib/systems/breathing/feedback.js.map +1 -1
  261. package/lib/systems/breathing/integration.js.map +1 -1
  262. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  263. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  264. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  265. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  266. package/lib/systems/combat/FallIntegration.js.map +1 -1
  267. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  268. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  269. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  270. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  271. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  272. package/lib/systems/combat/typeGuards.js.map +1 -1
  273. package/lib/systems/effects.js.map +1 -1
  274. package/lib/systems/game.js.map +1 -1
  275. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  276. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  277. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  278. package/lib/systems/movement/integration.js.map +1 -1
  279. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  280. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  281. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  282. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  283. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  284. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  285. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  286. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  287. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  288. package/lib/systems/trigram/StanceManager.js.map +1 -1
  289. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  290. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  291. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  292. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  293. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  294. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  295. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  296. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  297. package/lib/systems/trigram/techniques/index.js.map +1 -1
  298. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  299. package/lib/systems/trigram/types.js.map +1 -1
  300. package/lib/systems/types.js.map +1 -1
  301. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  302. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  303. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  304. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  305. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  306. package/lib/types/AccessibilityTypes.js.map +1 -1
  307. package/lib/types/PhysicsTypes.js.map +1 -1
  308. package/lib/types/common.js.map +1 -1
  309. package/lib/types/constants/colors.js.map +1 -1
  310. package/lib/types/constants/designSystem.js.map +1 -1
  311. package/lib/types/constants/layout.js.map +1 -1
  312. package/lib/types/constants/performance.js.map +1 -1
  313. package/lib/types/constants/typography.js.map +1 -1
  314. package/lib/types/facial.js.map +1 -1
  315. package/lib/types/hand-animation.js.map +1 -1
  316. package/lib/types/injury.js.map +1 -1
  317. package/lib/types/physics.js.map +1 -1
  318. package/lib/types/skeletal.js.map +1 -1
  319. package/lib/types/techniqueId.js.map +1 -1
  320. package/lib/utils/accessibility.js.map +1 -1
  321. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  322. package/lib/utils/assetConfig.js.map +1 -1
  323. package/lib/utils/characterScaling.js.map +1 -1
  324. package/lib/utils/colorHelpers.js.map +1 -1
  325. package/lib/utils/colorUtils.js.map +1 -1
  326. package/lib/utils/combatReadiness.js.map +1 -1
  327. package/lib/utils/controlMapping.js.map +1 -1
  328. package/lib/utils/deviceDetection.js.map +1 -1
  329. package/lib/utils/effectUtils.js.map +1 -1
  330. package/lib/utils/fabricTextures.js.map +1 -1
  331. package/lib/utils/hapticFeedback.js.map +1 -1
  332. package/lib/utils/haptics.js.map +1 -1
  333. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  334. package/lib/utils/inputSystem.js.map +1 -1
  335. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  336. package/lib/utils/math.js.map +1 -1
  337. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  338. package/lib/utils/mobileUIUtils.js.map +1 -1
  339. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  340. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  341. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  342. package/lib/utils/performanceOptimization.js.map +1 -1
  343. package/lib/utils/player3DHelpers.js.map +1 -1
  344. package/lib/utils/playerUtils.js.map +1 -1
  345. package/lib/utils/responsiveLayout.js.map +1 -1
  346. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  347. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  348. package/lib/utils/safeAreaUtils.js.map +1 -1
  349. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  350. package/lib/utils/skeletonScaling.js.map +1 -1
  351. package/lib/utils/stanceHelpers.js.map +1 -1
  352. package/lib/utils/threeObjectPool.js.map +1 -1
  353. package/lib/utils/visualEffects.js.map +1 -1
  354. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"ActionButtons.js","names":[],"sources":["../../../../src/components/shared/mobile/ActionButtons.tsx"],"sourcesContent":["/**\n * ActionButtons Component\n *\n * Touch-optimized action buttons for combat (Attack and Block)\n * Provides tactile combat controls with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for screen readers\n * - Keyboard navigation (Enter, Space)\n * - Visible focus indicators (2px cyan border)\n * - 80x80px and 70x70px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/ActionButtons\n * @category Mobile Controls\n * @korean 액션 버튼\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, {\n useCallback,\n useState,\n useMemo,\n useRef,\n useEffect,\n} from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerOptimizedHaptic } from \"./HapticController\";\nimport {\n applyOptimizedUpdate,\n createTransformStyle,\n createFilterStyle,\n} from \"./TouchOptimizer\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * Event type for button interactions\n */\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for ActionButtons component\n */\nexport interface ActionButtonsProps {\n /** Callback when attack button is pressed */\n readonly onAttack: () => void;\n /** Callback when block button is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether buttons are disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from right in pixels (default: 20) */\n readonly right?: number;\n /** Opacity of buttons (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * ActionButtons Component\n *\n * Provides two primary combat action buttons:\n * - Attack Button (⚡): Primary combat action, 80x80px\n * - Block Button (🛡️): Defensive action, 70x70px\n *\n * Features:\n * - Touch-optimized with minimum 44x44px targets\n * - Attack button: 80x80px for primary action\n * - Block button: 70x70px for secondary action\n * - Visual feedback on press\n * - Haptic feedback for tactile response\n * - Korean cyberpunk theming\n * - Hold-to-block support\n *\n * Usage in Combat:\n * - Attack: Executes current stance technique\n * - Block: Activates defensive guard (hold for sustained block)\n *\n * @example\n * ```tsx\n * <ActionButtons\n * onAttack={() => executeTechnique()}\n * onBlock={(eventType) => {\n * if (eventType === 'start') {\n * activateBlock();\n * } else {\n * deactivateBlock();\n * }\n * }}\n * disabled={isPaused}\n * />\n * ```\n *\n * @public\n * @korean 액션버튼\n */\nconst ActionButtonsComponent: React.FC<ActionButtonsProps> = ({\n onAttack,\n onBlock,\n disabled = false,\n bottom = 34,\n right = 20,\n opacity = 0.8,\n}) => {\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n const [attackFocused, setAttackFocused] = useState(false);\n const [blockFocused, setBlockFocused] = useState(false);\n\n // Button refs for direct DOM manipulation (immediate visual feedback)\n const attackButtonRef = useRef<HTMLButtonElement>(null);\n const blockButtonRef = useRef<HTMLButtonElement>(null);\n\n // Throttle callbacks to ~60fps for performance\n const throttledOnAttack = useThrottle(onAttack, 16);\n const throttledOnBlock = useThrottle(onBlock, 16);\n\n /**\n * Handle attack button press with optimized latency (<16ms)\n * Uses direct DOM manipulation for immediate visual feedback\n */\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual update using optimized approach (<16ms)\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n // GPU-accelerated transform\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n // Deferred state update\n setAttackPressed(true);\n throttledOnAttack();\n triggerOptimizedHaptic(\"medium\");\n },\n );\n },\n [disabled, throttledOnAttack],\n );\n\n /**\n * Handle attack button release with optimized latency\n */\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual reset\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setAttackPressed(false);\n },\n );\n },\n [disabled],\n );\n\n /**\n * Handle block button press with optimized latency (<16ms)\n */\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual update\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n setBlockPressed(true);\n throttledOnBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Handle block button release with optimized latency\n */\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual reset\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setBlockPressed(false);\n throttledOnBlock(\"end\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Cleanup on unmount - reset any pending visual states.\n * Note: Captures button refs at effect creation time to avoid stale closures.\n * If the buttons unmount before cleanup runs, the captured variables will still\n * reference the original DOM elements (now potentially detached), so style changes\n * are harmless but may not be visible. The null checks primarily guard against\n * refs that were never set in the first place.\n */\n useEffect(() => {\n // Store refs in variables at effect creation time\n const attackButton = attackButtonRef.current;\n const blockButton = blockButtonRef.current;\n\n return () => {\n if (attackButton) {\n attackButton.style.transform = createTransformStyle(false);\n attackButton.style.filter = createFilterStyle(false);\n }\n if (blockButton) {\n blockButton.style.transform = createTransformStyle(false);\n blockButton.style.filter = createFilterStyle(false);\n }\n };\n }, []);\n\n /**\n * Handle keyboard navigation for attack button\n */\n const handleAttackKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setAttackPressed(true);\n onAttack();\n triggerOptimizedHaptic(\"medium\");\n // Release after brief delay\n setTimeout(() => setAttackPressed(false), 150);\n },\n });\n },\n [disabled, onAttack],\n );\n\n /**\n * Handle keyboard navigation for block button\n */\n const handleBlockKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setBlockPressed(true);\n onBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n // Release after brief delay\n setTimeout(() => {\n setBlockPressed(false);\n onBlock(\"end\");\n }, 150);\n },\n });\n },\n [disabled, onBlock],\n );\n\n // Extract RGB colors using shared utility\n const colors = useMemo(\n () => ({\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n blue: getColorRGB(KOREAN_COLORS.ACCENT_BLUE),\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n right: `${right}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"10px\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"action-buttons\"\n >\n {/* Primary Attack Button */}\n <button\n ref={attackButtonRef}\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n onKeyDown={handleAttackKeyDown}\n onFocus={() => setAttackFocused(true)}\n onBlur={() => setAttackFocused(false)}\n style={{\n width: \"80px\",\n height: \"80px\",\n borderRadius: \"50%\",\n background: attackPressed\n ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`\n : `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`,\n border: \"3px solid #fff\",\n fontSize: \"28px\",\n color: \"#000\",\n fontWeight: \"bold\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n // Note: transform and filter are managed via TouchOptimizer/applyOptimizedUpdate\n // for immediate visual feedback. React state-driven inline styles serve as baseline\n // values that are overridden during active touch interactions via direct DOM manipulation.\n transform: createTransformStyle(attackPressed, 0.95),\n filter: createFilterStyle(attackPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: attackPressed\n ? `0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 12px rgba(0, 0, 0, 0.5), 0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.6)`,\n ...getFocusStyle(attackFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"공격\", \"Attack\").label}\n aria-pressed={attackPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button */}\n <button\n ref={blockButtonRef}\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n onKeyDown={handleBlockKeyDown}\n onFocus={() => setBlockFocused(true)}\n onBlur={() => setBlockFocused(false)}\n style={{\n width: \"70px\",\n height: \"70px\",\n borderRadius: \"50%\",\n background: blockPressed\n ? `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`\n : `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.9)`,\n border: \"2px solid #fff\",\n fontSize: \"24px\",\n color: \"#fff\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n transform: createTransformStyle(blockPressed, 0.95),\n filter: createFilterStyle(blockPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: blockPressed\n ? `0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 10px rgba(0, 0, 0, 0.5), 0 0 12px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.6)`,\n ...getFocusStyle(blockFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"방어\", \"Block\").label}\n aria-pressed={blockPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"block-button\"\n >\n 🛡️\n </button>\n\n {/* Button Labels (Korean + English) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n alignItems: \"center\",\n fontSize: \"10px\",\n color: `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n marginTop: \"4px\",\n }}\n >\n <span>공격 | Attack</span>\n <span style={{ fontSize: \"9px\" }}>방어 | Block</span>\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized ActionButtons with custom comparison\n * Only re-renders when props change\n */\nexport const ActionButtons = React.memo(\n ActionButtonsComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.right === nextProps.right &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onAttack === nextProps.onAttack &&\n prevProps.onBlock === nextProps.onBlock\n );\n },\n);\n\nActionButtons.displayName = \"ActionButtons\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"ActionButtons.js","names":[],"sources":["../../../../src/components/shared/mobile/ActionButtons.tsx"],"sourcesContent":["/**\n * ActionButtons Component\n *\n * Touch-optimized action buttons for combat (Attack and Block)\n * Provides tactile combat controls with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for screen readers\n * - Keyboard navigation (Enter, Space)\n * - Visible focus indicators (2px cyan border)\n * - 80x80px and 70x70px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/ActionButtons\n * @category Mobile Controls\n * @korean 액션 버튼\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, {\n useCallback,\n useState,\n useMemo,\n useRef,\n useEffect,\n} from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerOptimizedHaptic } from \"./HapticController\";\nimport {\n applyOptimizedUpdate,\n createTransformStyle,\n createFilterStyle,\n} from \"./TouchOptimizer\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * Event type for button interactions\n */\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for ActionButtons component\n */\nexport interface ActionButtonsProps {\n /** Callback when attack button is pressed */\n readonly onAttack: () => void;\n /** Callback when block button is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether buttons are disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from right in pixels (default: 20) */\n readonly right?: number;\n /** Opacity of buttons (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * ActionButtons Component\n *\n * Provides two primary combat action buttons:\n * - Attack Button (⚡): Primary combat action, 80x80px\n * - Block Button (🛡️): Defensive action, 70x70px\n *\n * Features:\n * - Touch-optimized with minimum 44x44px targets\n * - Attack button: 80x80px for primary action\n * - Block button: 70x70px for secondary action\n * - Visual feedback on press\n * - Haptic feedback for tactile response\n * - Korean cyberpunk theming\n * - Hold-to-block support\n *\n * Usage in Combat:\n * - Attack: Executes current stance technique\n * - Block: Activates defensive guard (hold for sustained block)\n *\n * @example\n * ```tsx\n * <ActionButtons\n * onAttack={() => executeTechnique()}\n * onBlock={(eventType) => {\n * if (eventType === 'start') {\n * activateBlock();\n * } else {\n * deactivateBlock();\n * }\n * }}\n * disabled={isPaused}\n * />\n * ```\n *\n * @public\n * @korean 액션버튼\n */\nconst ActionButtonsComponent: React.FC<ActionButtonsProps> = ({\n onAttack,\n onBlock,\n disabled = false,\n bottom = 34,\n right = 20,\n opacity = 0.8,\n}) => {\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n const [attackFocused, setAttackFocused] = useState(false);\n const [blockFocused, setBlockFocused] = useState(false);\n\n // Button refs for direct DOM manipulation (immediate visual feedback)\n const attackButtonRef = useRef<HTMLButtonElement>(null);\n const blockButtonRef = useRef<HTMLButtonElement>(null);\n\n // Throttle callbacks to ~60fps for performance\n const throttledOnAttack = useThrottle(onAttack, 16);\n const throttledOnBlock = useThrottle(onBlock, 16);\n\n /**\n * Handle attack button press with optimized latency (<16ms)\n * Uses direct DOM manipulation for immediate visual feedback\n */\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual update using optimized approach (<16ms)\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n // GPU-accelerated transform\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n // Deferred state update\n setAttackPressed(true);\n throttledOnAttack();\n triggerOptimizedHaptic(\"medium\");\n },\n );\n },\n [disabled, throttledOnAttack],\n );\n\n /**\n * Handle attack button release with optimized latency\n */\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual reset\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setAttackPressed(false);\n },\n );\n },\n [disabled],\n );\n\n /**\n * Handle block button press with optimized latency (<16ms)\n */\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual update\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n setBlockPressed(true);\n throttledOnBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Handle block button release with optimized latency\n */\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Immediate visual reset\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setBlockPressed(false);\n throttledOnBlock(\"end\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Cleanup on unmount - reset any pending visual states.\n * Note: Captures button refs at effect creation time to avoid stale closures.\n * If the buttons unmount before cleanup runs, the captured variables will still\n * reference the original DOM elements (now potentially detached), so style changes\n * are harmless but may not be visible. The null checks primarily guard against\n * refs that were never set in the first place.\n */\n useEffect(() => {\n // Store refs in variables at effect creation time\n const attackButton = attackButtonRef.current;\n const blockButton = blockButtonRef.current;\n\n return () => {\n if (attackButton) {\n attackButton.style.transform = createTransformStyle(false);\n attackButton.style.filter = createFilterStyle(false);\n }\n if (blockButton) {\n blockButton.style.transform = createTransformStyle(false);\n blockButton.style.filter = createFilterStyle(false);\n }\n };\n }, []);\n\n /**\n * Handle keyboard navigation for attack button\n */\n const handleAttackKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setAttackPressed(true);\n onAttack();\n triggerOptimizedHaptic(\"medium\");\n // Release after brief delay\n setTimeout(() => setAttackPressed(false), 150);\n },\n });\n },\n [disabled, onAttack],\n );\n\n /**\n * Handle keyboard navigation for block button\n */\n const handleBlockKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setBlockPressed(true);\n onBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n // Release after brief delay\n setTimeout(() => {\n setBlockPressed(false);\n onBlock(\"end\");\n }, 150);\n },\n });\n },\n [disabled, onBlock],\n );\n\n // Extract RGB colors using shared utility\n const colors = useMemo(\n () => ({\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n blue: getColorRGB(KOREAN_COLORS.ACCENT_BLUE),\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n right: `${right}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"10px\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"action-buttons\"\n >\n {/* Primary Attack Button */}\n <button\n ref={attackButtonRef}\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n onKeyDown={handleAttackKeyDown}\n onFocus={() => setAttackFocused(true)}\n onBlur={() => setAttackFocused(false)}\n style={{\n width: \"80px\",\n height: \"80px\",\n borderRadius: \"50%\",\n background: attackPressed\n ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`\n : `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`,\n border: \"3px solid #fff\",\n fontSize: \"28px\",\n color: \"#000\",\n fontWeight: \"bold\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n // Note: transform and filter are managed via TouchOptimizer/applyOptimizedUpdate\n // for immediate visual feedback. React state-driven inline styles serve as baseline\n // values that are overridden during active touch interactions via direct DOM manipulation.\n transform: createTransformStyle(attackPressed, 0.95),\n filter: createFilterStyle(attackPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: attackPressed\n ? `0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 12px rgba(0, 0, 0, 0.5), 0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.6)`,\n ...getFocusStyle(attackFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"공격\", \"Attack\").label}\n aria-pressed={attackPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button */}\n <button\n ref={blockButtonRef}\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n onKeyDown={handleBlockKeyDown}\n onFocus={() => setBlockFocused(true)}\n onBlur={() => setBlockFocused(false)}\n style={{\n width: \"70px\",\n height: \"70px\",\n borderRadius: \"50%\",\n background: blockPressed\n ? `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`\n : `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.9)`,\n border: \"2px solid #fff\",\n fontSize: \"24px\",\n color: \"#fff\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n transform: createTransformStyle(blockPressed, 0.95),\n filter: createFilterStyle(blockPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: blockPressed\n ? `0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 10px rgba(0, 0, 0, 0.5), 0 0 12px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.6)`,\n ...getFocusStyle(blockFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"방어\", \"Block\").label}\n aria-pressed={blockPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"block-button\"\n >\n 🛡️\n </button>\n\n {/* Button Labels (Korean + English) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n alignItems: \"center\",\n fontSize: \"10px\",\n color: `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n marginTop: \"4px\",\n }}\n >\n <span>공격 | Attack</span>\n <span style={{ fontSize: \"9px\" }}>방어 | Block</span>\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized ActionButtons with custom comparison\n * Only re-renders when props change\n */\nexport const ActionButtons = React.memo(\n ActionButtonsComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.right === nextProps.right &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onAttack === nextProps.onAttack &&\n prevProps.onBlock === nextProps.onBlock\n );\n },\n);\n\nActionButtons.displayName = \"ActionButtons\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"GestureRecognizerPure.js","names":[],"sources":["../../../../src/components/shared/mobile/GestureRecognizerPure.tsx"],"sourcesContent":["/**\n * GestureRecognizerPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Visual overlay for gesture detection feedback\n * Displays swipe trails and multi-touch indicators\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * @module components/mobile/GestureRecognizerPure\n * @category Mobile Controls\n * @korean 제스처 인식기 (순수 DOM)\n */\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport {\n GestureEvent,\n useTouchControls,\n} from \"../../../hooks/useTouchControls\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\n\n/**\n * Props for GestureRecognizerPure component\n */\nexport interface GestureRecognizerPureProps {\n /** Callback when gesture is detected */\n readonly onGesture: (gesture: GestureEvent) => void;\n /** Whether gesture recognition is enabled */\n readonly enabled?: boolean;\n /** Whether to show visual feedback */\n readonly showFeedback?: boolean;\n /** Minimum swipe distance in pixels (default: 50) */\n readonly minSwipeDistance?: number;\n}\n\n/**\n * Visual feedback state for gestures\n */\ninterface GestureFeedback {\n readonly id: number;\n readonly type: string;\n readonly timestamp: number;\n readonly x: number;\n readonly y: number;\n readonly age?: number; // Cached age to avoid impure function calls during render\n}\n\n/**\n * GestureRecognizerPure Component\n *\n * Pure DOM gesture detection and visual feedback for mobile controls\n * Features:\n * - Swipe detection (4 directions)\n * - Two-finger tap detection\n * - Visual trail feedback\n * - Gesture type indicators\n * - Auto-fading feedback\n *\n * Gesture Mappings:\n * - Swipe Right: Advance toward opponent\n * - Swipe Left: Retreat from opponent\n * - Swipe Up: High attack mode\n * - Swipe Down: Low attack mode\n * - Two-Finger Tap: Vital point targeting mode\n *\n * @example\n * ```tsx\n * <GestureRecognizerPure\n * onGesture={(gesture) => {\n * console.log('Detected:', gesture.type);\n * handleGesture(gesture);\n * }}\n * enabled={!isPaused}\n * showFeedback={true}\n * />\n * ```\n *\n * @public\n * @korean 제스처인식기순수\n */\nexport const GestureRecognizerPure: React.FC<GestureRecognizerPureProps> = ({\n onGesture,\n enabled = true,\n showFeedback = true,\n minSwipeDistance = 50,\n}) => {\n const [feedbacks, setFeedbacks] = useState<GestureFeedback[]>([]);\n const [nextId, setNextId] = useState(0);\n\n /**\n * Handle detected gesture\n */\n const handleGesture = useCallback(\n (gesture: GestureEvent) => {\n // Pass gesture to parent\n onGesture(gesture);\n\n // Add visual feedback\n if (\n showFeedback &&\n gesture.endX !== undefined &&\n gesture.endY !== undefined\n ) {\n const feedback: GestureFeedback = {\n id: nextId,\n type: gesture.type,\n timestamp: Date.now(),\n x: gesture.endX,\n y: gesture.endY,\n };\n\n setFeedbacks((prev) => [...prev, feedback]);\n setNextId((prev) => prev + 1);\n }\n },\n [onGesture, showFeedback, nextId],\n );\n\n /**\n * Use touch controls hook for gesture detection\n */\n useTouchControls({\n onGesture: handleGesture,\n enabled,\n minSwipeDistance,\n });\n\n /**\n * Clean up old feedback indicators and update ages\n */\n useEffect(() => {\n if (!showFeedback) return;\n\n const interval = setInterval(() => {\n const now = Date.now();\n setFeedbacks((prev) =>\n prev\n .filter((fb) => now - fb.timestamp < 1000)\n .map((fb) => ({ ...fb, age: now - fb.timestamp })),\n );\n }, 100);\n\n return () => clearInterval(interval);\n }, [showFeedback]);\n\n if (!showFeedback) {\n return null;\n }\n\n // Get RGB colors using shared utility\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n\n /**\n * Get display info for gesture type\n */\n const getGestureDisplay = (\n type: string,\n ): { korean: string; english: string; icon: string } => {\n const displays: Record<\n string,\n { korean: string; english: string; icon: string }\n > = {\n \"swipe-right\": { korean: \"전진\", english: \"Advance\", icon: \"→\" },\n \"swipe-left\": { korean: \"후퇴\", english: \"Retreat\", icon: \"←\" },\n \"swipe-up\": { korean: \"상단\", english: \"High\", icon: \"↑\" },\n \"swipe-down\": { korean: \"하단\", english: \"Low\", icon: \"↓\" },\n \"two-finger-tap\": { korean: \"급소\", english: \"Vital Point\", icon: \"🎯\" },\n tap: { korean: \"터치\", english: \"Tap\", icon: \"👆\" },\n };\n return (\n displays[type] ?? { korean: \"제스처\", english: \"Gesture\", icon: \"✋\" }\n );\n };\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n data-testid=\"gesture-recognizer-pure\"\n >\n {/* Gesture feedback indicators */}\n {feedbacks.map((feedback) => {\n const age = feedback.age ?? 0;\n const opacity = Math.max(0, 1 - age / 1000);\n const scale = 1 + age / 500;\n const display = getGestureDisplay(feedback.type);\n\n return (\n <div\n key={feedback.id}\n style={{\n position: \"absolute\",\n left: `${feedback.x}px`,\n top: `${feedback.y}px`,\n transform: `translate(-50%, -50%) scale(${scale})`,\n opacity,\n transition: \"all 0.1s ease-out\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n }}\n data-testid={`gesture-feedback-pure-${feedback.id}`}\n >\n {/* Icon */}\n <div\n style={{\n fontSize: \"32px\",\n color: `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity})`,\n textShadow: `0 0 10px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity * 0.8})`,\n }}\n >\n {display.icon}\n </div>\n\n {/* Label */}\n <div\n style={{\n background: `rgba(0, 0, 0, ${opacity * 0.8})`,\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n borderRadius: \"8px\",\n padding: \"4px 8px\",\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n textAlign: \"center\",\n whiteSpace: \"nowrap\",\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {display.korean} | {display.english}\n </div>\n </div>\n );\n })}\n\n {/* Gesture instructions overlay (optional) */}\n {enabled && (\n <div\n style={{\n position: \"absolute\",\n top: \"10px\",\n right: \"10px\",\n background: \"rgba(0, 0, 0, 0.7)\",\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.6)`,\n borderRadius: \"8px\",\n padding: \"8px 12px\",\n fontSize: \"10px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n maxWidth: \"150px\",\n opacity: 0.7,\n }}\n data-testid=\"gesture-instructions-pure\"\n >\n <div style={{ fontWeight: \"bold\", marginBottom: \"4px\" }}>\n 제스처 | Gestures\n </div>\n <div>← → 이동 | Move</div>\n <div>↑ ↓ 공격 | Attack</div>\n <div>🤞 급소 | Vital</div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,IAAa,yBAA+D,EAC1E,WACA,UAAU,MACV,eAAe,MACf,mBAAmB,SACf;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAA4B,EAAE,CAAC;CACjE,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;;;;AAkCvC,kBAAiB;EACf,WA9BoB,aACnB,YAA0B;AAEzB,aAAU,QAAQ;AAGlB,OACE,gBACA,QAAQ,SAAS,KAAA,KACjB,QAAQ,SAAS,KAAA,GACjB;IACA,MAAM,WAA4B;KAChC,IAAI;KACJ,MAAM,QAAQ;KACd,WAAW,KAAK,KAAK;KACrB,GAAG,QAAQ;KACX,GAAG,QAAQ;KACZ;AAED,kBAAc,SAAS,CAAC,GAAG,MAAM,SAAS,CAAC;AAC3C,eAAW,SAAS,OAAO,EAAE;;KAGjC;GAAC;GAAW;GAAc;GAAO,CAOtB;EACX;EACA;EACD,CAAC;;;;AAKF,iBAAgB;AACd,MAAI,CAAC,aAAc;EAEnB,MAAM,WAAW,kBAAkB;GACjC,MAAM,MAAM,KAAK,KAAK;AACtB,iBAAc,SACZ,KACG,QAAQ,OAAO,MAAM,GAAG,YAAY,IAAK,CACzC,KAAK,QAAQ;IAAE,GAAG;IAAI,KAAK,MAAM,GAAG;IAAW,EAAE,CACrD;KACA,IAAI;AAEP,eAAa,cAAc,SAAS;IACnC,CAAC,aAAa,CAAC;AAElB,KAAI,CAAC,aACH,QAAO;CAIT,MAAM,eAAe,YAAY,cAAc,aAAa;CAC5D,MAAM,YAAY,YAAY,cAAc,YAAY;;;;CAKxD,MAAM,qBACJ,SACsD;AAYtD,SACE;GARA,eAAe;IAAE,QAAQ;IAAM,SAAS;IAAW,MAAM;IAAK;GAC9D,cAAc;IAAE,QAAQ;IAAM,SAAS;IAAW,MAAM;IAAK;GAC7D,YAAY;IAAE,QAAQ;IAAM,SAAS;IAAQ,MAAM;IAAK;GACxD,cAAc;IAAE,QAAQ;IAAM,SAAS;IAAO,MAAM;IAAK;GACzD,kBAAkB;IAAE,QAAQ;IAAM,SAAS;IAAe,MAAM;IAAM;GACtE,KAAK;IAAE,QAAQ;IAAM,SAAS;IAAO,MAAM;IAAM;GAGjD,CAAS,SAAS;GAAE,QAAQ;GAAO,SAAS;GAAW,MAAM;GAAK;;AAItE,QACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,eAAe;GACf,QAAQ;GACT;EACD,eAAY;YAVd,CAaG,UAAU,KAAK,aAAa;GAC3B,MAAM,MAAM,SAAS,OAAO;GAC5B,MAAM,UAAU,KAAK,IAAI,GAAG,IAAI,MAAM,IAAK;GAC3C,MAAM,QAAQ,IAAI,MAAM;GACxB,MAAM,UAAU,kBAAkB,SAAS,KAAK;AAEhD,UACE,qBAAC,OAAD;IAEE,OAAO;KACL,UAAU;KACV,MAAM,GAAG,SAAS,EAAE;KACpB,KAAK,GAAG,SAAS,EAAE;KACnB,WAAW,+BAA+B,MAAM;KAChD;KACA,YAAY;KACZ,SAAS;KACT,eAAe;KACf,YAAY;KACZ,KAAK;KACN;IACD,eAAa,yBAAyB,SAAS;cAdjD,CAiBE,oBAAC,OAAD;KACE,OAAO;MACL,UAAU;MACV,OAAO,QAAQ,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,QAAQ;MACvE,YAAY,iBAAiB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,GAAI;MAC5F;eAEA,QAAQ;KACL,CAAA,EAGN,qBAAC,OAAD;KACE,OAAO;MACL,YAAY,iBAAiB,UAAU,GAAI;MAC3C,QAAQ,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,QAAQ;MAC3F,cAAc;MACd,SAAS;MACT,UAAU;MACV,YAAY;MACZ,OAAO,QAAQ,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,QAAQ;MAChF,WAAW;MACX,YAAY;MACZ,YAAY;MACb;eAZH;MAcG,QAAQ;MAAO;MAAI,QAAQ;MACxB;OACF;MA3CC,SAAS,GA2CV;IAER,EAGD,WACC,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,KAAK;IACL,OAAO;IACP,YAAY;IACZ,QAAQ,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE;IAC/E,cAAc;IACd,SAAS;IACT,UAAU;IACV,OAAO,QAAQ,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE;IACpE,UAAU;IACV,SAAS;IACV;GACD,eAAY;aAdd;IAgBE,oBAAC,OAAD;KAAK,OAAO;MAAE,YAAY;MAAQ,cAAc;MAAO;eAAE;KAEnD,CAAA;IACN,oBAAC,OAAD,EAAA,UAAK,iBAAmB,CAAA;IACxB,oBAAC,OAAD,EAAA,UAAK,mBAAqB,CAAA;IAC1B,oBAAC,OAAD,EAAA,UAAK,iBAAmB,CAAA;IACpB;KAEJ"}
1
+ {"version":3,"file":"GestureRecognizerPure.js","names":[],"sources":["../../../../src/components/shared/mobile/GestureRecognizerPure.tsx"],"sourcesContent":["/**\n * GestureRecognizerPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Visual overlay for gesture detection feedback\n * Displays swipe trails and multi-touch indicators\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * @module components/mobile/GestureRecognizerPure\n * @category Mobile Controls\n * @korean 제스처 인식기 (순수 DOM)\n */\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport {\n GestureEvent,\n useTouchControls,\n} from \"../../../hooks/useTouchControls\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\n\n/**\n * Props for GestureRecognizerPure component\n */\nexport interface GestureRecognizerPureProps {\n /** Callback when gesture is detected */\n readonly onGesture: (gesture: GestureEvent) => void;\n /** Whether gesture recognition is enabled */\n readonly enabled?: boolean;\n /** Whether to show visual feedback */\n readonly showFeedback?: boolean;\n /** Minimum swipe distance in pixels (default: 50) */\n readonly minSwipeDistance?: number;\n}\n\n/**\n * Visual feedback state for gestures\n */\ninterface GestureFeedback {\n readonly id: number;\n readonly type: string;\n readonly timestamp: number;\n readonly x: number;\n readonly y: number;\n readonly age?: number; // Cached age to avoid impure function calls during render\n}\n\n/**\n * GestureRecognizerPure Component\n *\n * Pure DOM gesture detection and visual feedback for mobile controls\n * Features:\n * - Swipe detection (4 directions)\n * - Two-finger tap detection\n * - Visual trail feedback\n * - Gesture type indicators\n * - Auto-fading feedback\n *\n * Gesture Mappings:\n * - Swipe Right: Advance toward opponent\n * - Swipe Left: Retreat from opponent\n * - Swipe Up: High attack mode\n * - Swipe Down: Low attack mode\n * - Two-Finger Tap: Vital point targeting mode\n *\n * @example\n * ```tsx\n * <GestureRecognizerPure\n * onGesture={(gesture) => {\n * console.log('Detected:', gesture.type);\n * handleGesture(gesture);\n * }}\n * enabled={!isPaused}\n * showFeedback={true}\n * />\n * ```\n *\n * @public\n * @korean 제스처인식기순수\n */\nexport const GestureRecognizerPure: React.FC<GestureRecognizerPureProps> = ({\n onGesture,\n enabled = true,\n showFeedback = true,\n minSwipeDistance = 50,\n}) => {\n const [feedbacks, setFeedbacks] = useState<GestureFeedback[]>([]);\n const [nextId, setNextId] = useState(0);\n\n /**\n * Handle detected gesture\n */\n const handleGesture = useCallback(\n (gesture: GestureEvent) => {\n // Pass gesture to parent\n onGesture(gesture);\n\n // Add visual feedback\n if (\n showFeedback &&\n gesture.endX !== undefined &&\n gesture.endY !== undefined\n ) {\n const feedback: GestureFeedback = {\n id: nextId,\n type: gesture.type,\n timestamp: Date.now(),\n x: gesture.endX,\n y: gesture.endY,\n };\n\n setFeedbacks((prev) => [...prev, feedback]);\n setNextId((prev) => prev + 1);\n }\n },\n [onGesture, showFeedback, nextId],\n );\n\n /**\n * Use touch controls hook for gesture detection\n */\n useTouchControls({\n onGesture: handleGesture,\n enabled,\n minSwipeDistance,\n });\n\n /**\n * Clean up old feedback indicators and update ages\n */\n useEffect(() => {\n if (!showFeedback) return;\n\n const interval = setInterval(() => {\n const now = Date.now();\n setFeedbacks((prev) =>\n prev\n .filter((fb) => now - fb.timestamp < 1000)\n .map((fb) => ({ ...fb, age: now - fb.timestamp })),\n );\n }, 100);\n\n return () => clearInterval(interval);\n }, [showFeedback]);\n\n if (!showFeedback) {\n return null;\n }\n\n // Get RGB colors using shared utility\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n\n /**\n * Get display info for gesture type\n */\n const getGestureDisplay = (\n type: string,\n ): { korean: string; english: string; icon: string } => {\n const displays: Record<\n string,\n { korean: string; english: string; icon: string }\n > = {\n \"swipe-right\": { korean: \"전진\", english: \"Advance\", icon: \"→\" },\n \"swipe-left\": { korean: \"후퇴\", english: \"Retreat\", icon: \"←\" },\n \"swipe-up\": { korean: \"상단\", english: \"High\", icon: \"↑\" },\n \"swipe-down\": { korean: \"하단\", english: \"Low\", icon: \"↓\" },\n \"two-finger-tap\": { korean: \"급소\", english: \"Vital Point\", icon: \"🎯\" },\n tap: { korean: \"터치\", english: \"Tap\", icon: \"👆\" },\n };\n return (\n displays[type] ?? { korean: \"제스처\", english: \"Gesture\", icon: \"✋\" }\n );\n };\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n data-testid=\"gesture-recognizer-pure\"\n >\n {/* Gesture feedback indicators */}\n {feedbacks.map((feedback) => {\n const age = feedback.age ?? 0;\n const opacity = Math.max(0, 1 - age / 1000);\n const scale = 1 + age / 500;\n const display = getGestureDisplay(feedback.type);\n\n return (\n <div\n key={feedback.id}\n style={{\n position: \"absolute\",\n left: `${feedback.x}px`,\n top: `${feedback.y}px`,\n transform: `translate(-50%, -50%) scale(${scale})`,\n opacity,\n transition: \"all 0.1s ease-out\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n }}\n data-testid={`gesture-feedback-pure-${feedback.id}`}\n >\n {/* Icon */}\n <div\n style={{\n fontSize: \"32px\",\n color: `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity})`,\n textShadow: `0 0 10px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity * 0.8})`,\n }}\n >\n {display.icon}\n </div>\n\n {/* Label */}\n <div\n style={{\n background: `rgba(0, 0, 0, ${opacity * 0.8})`,\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n borderRadius: \"8px\",\n padding: \"4px 8px\",\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n textAlign: \"center\",\n whiteSpace: \"nowrap\",\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {display.korean} | {display.english}\n </div>\n </div>\n );\n })}\n\n {/* Gesture instructions overlay (optional) */}\n {enabled && (\n <div\n style={{\n position: \"absolute\",\n top: \"10px\",\n right: \"10px\",\n background: \"rgba(0, 0, 0, 0.7)\",\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.6)`,\n borderRadius: \"8px\",\n padding: \"8px 12px\",\n fontSize: \"10px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n maxWidth: \"150px\",\n opacity: 0.7,\n }}\n data-testid=\"gesture-instructions-pure\"\n >\n <div style={{ fontWeight: \"bold\", marginBottom: \"4px\" }}>\n 제스처 | Gestures\n </div>\n <div>← → 이동 | Move</div>\n <div>↑ ↓ 공격 | Attack</div>\n <div>🤞 급소 | Vital</div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"HapticController.js","names":[],"sources":["../../../../src/components/shared/mobile/HapticController.ts"],"sourcesContent":["/**\n * HapticController\n * \n * Optimized haptic feedback system with device capability detection\n * Provides differentiated haptic patterns without causing frame drops\n * \n * Key Features:\n * - Device capability detection (auto-disable on low-end devices)\n * - Differentiated feedback intensities (light, medium, strong)\n * - Frame-drop prevention (<2ms overhead)\n * - Adaptive patterns based on device performance\n * - Throttling to prevent excessive haptic triggers\n * \n * Browser Compatibility Note:\n * navigator.vibrate() returns boolean in some browsers (Chrome, Edge) and void in others (Firefox).\n * This implementation normalizes return values to boolean for consistency.\n * \n * @module components/mobile/HapticController\n * @category Mobile Controls\n * @korean 햅틱 컨트롤러\n */\n\n/**\n * Haptic intensity levels\n */\nexport type HapticIntensity = 'light' | 'medium' | 'strong' | 'disabled';\n\n/**\n * Device performance tier\n */\nexport type DevicePerformanceTier = 'high' | 'medium' | 'low';\n\n/**\n * Haptic pattern configuration\n */\ninterface HapticPattern {\n readonly vibration: number | number[];\n readonly maxFrameTime: number; // Maximum frame time impact in ms\n}\n\n/**\n * Optimized haptic patterns for different intensities\n * Designed to provide clear feedback without frame drops\n * \n * @korean 햅틱 패턴\n */\nconst HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [20], // Short, crisp tap\n maxFrameTime: 0.5, // Minimal impact\n },\n medium: {\n vibration: [40], // Medium pulse\n maxFrameTime: 1.0, // Small impact\n },\n strong: {\n vibration: [60], // Strong pulse\n maxFrameTime: 2.0, // Noticeable impact\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * Adaptive haptic patterns for low-end devices\n * Reduced intensities to prevent frame drops\n * \n * @korean 적응형 햅틱 패턴\n */\nconst ADAPTIVE_HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [10], // Minimal vibration\n maxFrameTime: 0.2,\n },\n medium: {\n vibration: [20], // Reduced medium\n maxFrameTime: 0.5,\n },\n strong: {\n vibration: [30], // Reduced strong\n maxFrameTime: 1.0,\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * HapticController class\n * Manages haptic feedback with device-aware optimization\n * \n * @public\n * @korean 햅틱컨트롤러\n */\nexport class HapticController {\n private static instance: HapticController | null = null;\n private isSupported: boolean = false;\n private isEnabled: boolean = true;\n private performanceTier: DevicePerformanceTier = 'high';\n /**\n * Last trigger timestamp for throttling.\n * Initialized to -Infinity to ensure the first trigger always succeeds.\n * Using -Infinity instead of 0 prevents throttling when performance.now() returns 0,\n * which can occur in test environments or immediately after page load.\n */\n private lastTriggerTime: number = -Infinity;\n private minTriggerInterval: number = 50; // Minimum 50ms between haptics\n\n /**\n * Private constructor for singleton pattern\n */\n private constructor() {\n this.isSupported = this.detectHapticSupport();\n this.performanceTier = this.detectPerformanceTier();\n \n // Disable haptics on low-end devices by default\n if (this.performanceTier === 'low') {\n this.isEnabled = false;\n }\n }\n\n /**\n * Get singleton instance\n * \n * @returns HapticController instance\n * @korean 인스턴스가져오기\n */\n public static getInstance(): HapticController {\n this.instance ??= new HapticController();\n return this.instance;\n }\n\n /**\n * Detect if haptic feedback is supported\n * \n * @returns True if Vibration API is available\n * @korean 햅틱지원감지\n */\n private detectHapticSupport(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n // Check for Vibration API\n return 'vibrate' in navigator;\n }\n\n /**\n * Detect device performance tier\n * Uses multiple heuristics to determine device capability\n * \n * @returns Performance tier (high, medium, low)\n * @korean 성능등급감지\n */\n private detectPerformanceTier(): DevicePerformanceTier {\n if (typeof navigator === 'undefined') {\n return 'medium';\n }\n\n // Check navigator.hardwareConcurrency (CPU cores)\n const cores = navigator.hardwareConcurrency ?? 4;\n \n // Check device memory (if available)\n const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 4;\n \n // Check if running on mobile\n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\n \n // Heuristic scoring\n let score = 0;\n \n // More cores = better performance\n if (cores >= 8) score += 2;\n else if (cores >= 4) score += 1;\n \n // More memory = better performance\n if (memory >= 8) score += 2;\n else if (memory >= 4) score += 1;\n \n // Desktop generally performs better\n if (!isMobile) score += 1;\n \n // Determine tier\n if (score >= 4) return 'high';\n if (score >= 2) return 'medium';\n return 'low';\n }\n\n /**\n * Trigger haptic feedback with optimized patterns\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Light haptic for button tap\n * haptic.trigger('light');\n * \n * // Strong haptic for critical hit\n * haptic.trigger('strong');\n * ```\n * \n * @public\n */\n public trigger(intensity: HapticIntensity): boolean {\n // Check if haptics are supported and enabled\n if (!this.isSupported || !this.isEnabled || intensity === 'disabled') {\n return false;\n }\n\n // Throttle haptic triggers to prevent excessive vibration\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n // Select pattern based on device performance\n const patterns = this.performanceTier === 'low' \n ? ADAPTIVE_HAPTIC_PATTERNS \n : HAPTIC_PATTERNS;\n \n const pattern = patterns[intensity];\n\n try {\n // Trigger vibration (see class-level comment for browser compatibility notes)\n const result = navigator.vibrate(pattern.vibration);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Custom combo pattern\n * haptic.triggerCustom([30, 20, 30, 20, 50]);\n * ```\n * \n * @public\n */\n public triggerCustom(pattern: number | number[]): boolean {\n if (!this.isSupported || !this.isEnabled) {\n return false;\n }\n\n // Throttle haptic triggers\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n try {\n // Adapt pattern for low-end devices (reduce durations by 50%)\n let adaptedPattern = pattern;\n if (this.performanceTier === 'low') {\n if (Array.isArray(pattern)) {\n adaptedPattern = pattern.map(duration => Math.floor(duration * 0.5));\n } else {\n adaptedPattern = Math.floor(pattern * 0.5);\n }\n }\n\n // Trigger vibration (see class-level comment for browser compatibility notes)\n const result = navigator.vibrate(adaptedPattern);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Custom haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Stop any ongoing haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * \n * @public\n */\n public stop(): boolean {\n if (!this.isSupported) {\n return false;\n }\n\n try {\n // Stop vibration (see class-level comment for browser compatibility notes)\n const result = navigator.vibrate(0);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Failed to stop haptic feedback:', error);\n return false;\n }\n }\n\n /**\n * Enable haptic feedback\n * \n * @korean 햅틱활성화\n * @public\n */\n public enable(): void {\n this.isEnabled = true;\n }\n\n /**\n * Disable haptic feedback\n * \n * @korean 햅틱비활성화\n * @public\n */\n public disable(): void {\n this.isEnabled = false;\n this.stop();\n }\n\n /**\n * Check if haptic is currently enabled\n * \n * @returns True if haptic is enabled\n * @korean 햅틱활성화상태\n * @public\n */\n public isHapticEnabled(): boolean {\n return this.isSupported && this.isEnabled;\n }\n\n /**\n * Get current device performance tier\n * \n * @returns Performance tier\n * @korean 성능등급가져오기\n * @public\n */\n public getPerformanceTier(): DevicePerformanceTier {\n return this.performanceTier;\n }\n\n /**\n * Set minimum interval between haptic triggers\n * \n * @param intervalMs - Minimum interval in milliseconds\n * @korean 최소간격설정\n * @public\n */\n public setMinTriggerInterval(intervalMs: number): void {\n this.minTriggerInterval = Math.max(0, intervalMs);\n }\n}\n\n/**\n * Convenience function to trigger haptic feedback\n * Uses singleton HapticController instance\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * // Light haptic for UI interaction\n * triggerOptimizedHaptic('light');\n * \n * // Medium haptic for combat action\n * triggerOptimizedHaptic('medium');\n * \n * // Strong haptic for critical hit\n * triggerOptimizedHaptic('strong');\n * ```\n * \n * @public\n */\nexport function triggerOptimizedHaptic(intensity: HapticIntensity): boolean {\n return HapticController.getInstance().trigger(intensity);\n}\n\n/**\n * Convenience function to trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @public\n */\nexport function triggerCustomOptimizedHaptic(pattern: number | number[]): boolean {\n return HapticController.getInstance().triggerCustom(pattern);\n}\n\n/**\n * Convenience function to stop haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * @public\n */\nexport function stopOptimizedHaptic(): boolean {\n return HapticController.getInstance().stop();\n}\n\n/**\n * Combat-specific optimized haptic patterns\n * Pre-configured for common combat scenarios\n * \n * @korean 전투 햅틱 패턴\n * @public\n */\nexport const OptimizedCombatHaptics = {\n /**\n * Standard attack hit feedback\n * @korean 일반 공격\n */\n attack: () => triggerOptimizedHaptic('medium'),\n\n /**\n * Block successful feedback\n * @korean 방어 성공\n */\n block: () => triggerOptimizedHaptic('light'),\n\n /**\n * Critical hit feedback with double pulse\n * @korean 크리티컬 히트\n */\n criticalHit: () => triggerCustomOptimizedHaptic([40, 20, 60]),\n\n /**\n * Vital point strike feedback\n * @korean 급소 타격\n */\n vitalPointStrike: () => triggerOptimizedHaptic('strong'),\n\n /**\n * Stance change feedback\n * @korean 자세 변경\n */\n stanceChange: () => triggerOptimizedHaptic('light'),\n\n /**\n * Combo counter increment\n * @korean 콤보 카운터\n */\n comboIncrement: () => triggerOptimizedHaptic('light'),\n\n /**\n * Player KO feedback with extended pattern\n * @korean 플레이어 KO\n */\n knockout: () => triggerCustomOptimizedHaptic([60, 30, 60, 30, 100]),\n\n /**\n * Error or invalid action feedback\n * @korean 오류 피드백\n */\n error: () => triggerCustomOptimizedHaptic([15, 10, 15]),\n} as const;\n"],"mappings":"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"}
1
+ {"version":3,"file":"HapticController.js","names":[],"sources":["../../../../src/components/shared/mobile/HapticController.ts"],"sourcesContent":["/**\n * HapticController\n * \n * Optimized haptic feedback system with device capability detection\n * Provides differentiated haptic patterns without causing frame drops\n * \n * Key Features:\n * - Device capability detection (auto-disable on low-end devices)\n * - Differentiated feedback intensities (light, medium, strong)\n * - Frame-drop prevention (<2ms overhead)\n * - Adaptive patterns based on device performance\n * - Throttling to prevent excessive haptic triggers\n * \n * Browser Compatibility Note:\n * navigator.vibrate() returns boolean in some browsers (Chrome, Edge) and void in others (Firefox).\n * This implementation normalizes return values to boolean for consistency.\n * \n * @module components/mobile/HapticController\n * @category Mobile Controls\n * @korean 햅틱 컨트롤러\n */\n\n/**\n * Haptic intensity levels\n */\nexport type HapticIntensity = 'light' | 'medium' | 'strong' | 'disabled';\n\n/**\n * Device performance tier\n */\nexport type DevicePerformanceTier = 'high' | 'medium' | 'low';\n\n/**\n * Haptic pattern configuration\n */\ninterface HapticPattern {\n readonly vibration: number | number[];\n readonly maxFrameTime: number; // Maximum frame time impact in ms\n}\n\n/**\n * Optimized haptic patterns for different intensities\n * Designed to provide clear feedback without frame drops\n * \n * @korean 햅틱 패턴\n */\nconst HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [20], // Short, crisp tap\n maxFrameTime: 0.5, // Minimal impact\n },\n medium: {\n vibration: [40], // Medium pulse\n maxFrameTime: 1.0, // Small impact\n },\n strong: {\n vibration: [60], // Strong pulse\n maxFrameTime: 2.0, // Noticeable impact\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * Adaptive haptic patterns for low-end devices\n * Reduced intensities to prevent frame drops\n * \n * @korean 적응형 햅틱 패턴\n */\nconst ADAPTIVE_HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [10], // Minimal vibration\n maxFrameTime: 0.2,\n },\n medium: {\n vibration: [20], // Reduced medium\n maxFrameTime: 0.5,\n },\n strong: {\n vibration: [30], // Reduced strong\n maxFrameTime: 1.0,\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * HapticController class\n * Manages haptic feedback with device-aware optimization\n * \n * @public\n * @korean 햅틱컨트롤러\n */\nexport class HapticController {\n private static instance: HapticController | null = null;\n private isSupported: boolean = false;\n private isEnabled: boolean = true;\n private performanceTier: DevicePerformanceTier = 'high';\n /**\n * Last trigger timestamp for throttling.\n * Initialized to -Infinity to ensure the first trigger always succeeds.\n * Using -Infinity instead of 0 prevents throttling when performance.now() returns 0,\n * which can occur in test environments or immediately after page load.\n */\n private lastTriggerTime: number = -Infinity;\n private minTriggerInterval: number = 50; // Minimum 50ms between haptics\n\n /**\n * Private constructor for singleton pattern\n */\n private constructor() {\n this.isSupported = this.detectHapticSupport();\n this.performanceTier = this.detectPerformanceTier();\n \n // Disable haptics on low-end devices by default\n if (this.performanceTier === 'low') {\n this.isEnabled = false;\n }\n }\n\n /**\n * Get singleton instance\n * \n * @returns HapticController instance\n * @korean 인스턴스가져오기\n */\n public static getInstance(): HapticController {\n this.instance ??= new HapticController();\n return this.instance;\n }\n\n /**\n * Detect if haptic feedback is supported\n * \n * @returns True if Vibration API is available\n * @korean 햅틱지원감지\n */\n private detectHapticSupport(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n // Check for Vibration API\n return 'vibrate' in navigator;\n }\n\n /**\n * Detect device performance tier\n * Uses multiple heuristics to determine device capability\n * \n * @returns Performance tier (high, medium, low)\n * @korean 성능등급감지\n */\n private detectPerformanceTier(): DevicePerformanceTier {\n if (typeof navigator === 'undefined') {\n return 'medium';\n }\n\n // Check navigator.hardwareConcurrency (CPU cores)\n const cores = navigator.hardwareConcurrency ?? 4;\n \n // Check device memory (if available)\n const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 4;\n \n // Check if running on mobile\n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\n \n // Heuristic scoring\n let score = 0;\n \n // More cores = better performance\n if (cores >= 8) score += 2;\n else if (cores >= 4) score += 1;\n \n // More memory = better performance\n if (memory >= 8) score += 2;\n else if (memory >= 4) score += 1;\n \n // Desktop generally performs better\n if (!isMobile) score += 1;\n \n // Determine tier\n if (score >= 4) return 'high';\n if (score >= 2) return 'medium';\n return 'low';\n }\n\n /**\n * Trigger haptic feedback with optimized patterns\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Light haptic for button tap\n * haptic.trigger('light');\n * \n * // Strong haptic for critical hit\n * haptic.trigger('strong');\n * ```\n * \n * @public\n */\n public trigger(intensity: HapticIntensity): boolean {\n // Check if haptics are supported and enabled\n if (!this.isSupported || !this.isEnabled || intensity === 'disabled') {\n return false;\n }\n\n // Throttle haptic triggers to prevent excessive vibration\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n // Select pattern based on device performance\n const patterns = this.performanceTier === 'low' \n ? ADAPTIVE_HAPTIC_PATTERNS \n : HAPTIC_PATTERNS;\n \n const pattern = patterns[intensity];\n\n try {\n // Trigger vibration (see class-level comment for browser compatibility notes)\n const result = navigator.vibrate(pattern.vibration);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Custom combo pattern\n * haptic.triggerCustom([30, 20, 30, 20, 50]);\n * ```\n * \n * @public\n */\n public triggerCustom(pattern: number | number[]): boolean {\n if (!this.isSupported || !this.isEnabled) {\n return false;\n }\n\n // Throttle haptic triggers\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n try {\n // Adapt pattern for low-end devices (reduce durations by 50%)\n let adaptedPattern = pattern;\n if (this.performanceTier === 'low') {\n if (Array.isArray(pattern)) {\n adaptedPattern = pattern.map(duration => Math.floor(duration * 0.5));\n } else {\n adaptedPattern = Math.floor(pattern * 0.5);\n }\n }\n\n // Trigger vibration (see class-level comment for browser compatibility notes)\n const result = navigator.vibrate(adaptedPattern);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Custom haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Stop any ongoing haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * \n * @public\n */\n public stop(): boolean {\n if (!this.isSupported) {\n return false;\n }\n\n try {\n // Stop vibration (see class-level comment for browser compatibility notes)\n const result = navigator.vibrate(0);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Failed to stop haptic feedback:', error);\n return false;\n }\n }\n\n /**\n * Enable haptic feedback\n * \n * @korean 햅틱활성화\n * @public\n */\n public enable(): void {\n this.isEnabled = true;\n }\n\n /**\n * Disable haptic feedback\n * \n * @korean 햅틱비활성화\n * @public\n */\n public disable(): void {\n this.isEnabled = false;\n this.stop();\n }\n\n /**\n * Check if haptic is currently enabled\n * \n * @returns True if haptic is enabled\n * @korean 햅틱활성화상태\n * @public\n */\n public isHapticEnabled(): boolean {\n return this.isSupported && this.isEnabled;\n }\n\n /**\n * Get current device performance tier\n * \n * @returns Performance tier\n * @korean 성능등급가져오기\n * @public\n */\n public getPerformanceTier(): DevicePerformanceTier {\n return this.performanceTier;\n }\n\n /**\n * Set minimum interval between haptic triggers\n * \n * @param intervalMs - Minimum interval in milliseconds\n * @korean 최소간격설정\n * @public\n */\n public setMinTriggerInterval(intervalMs: number): void {\n this.minTriggerInterval = Math.max(0, intervalMs);\n }\n}\n\n/**\n * Convenience function to trigger haptic feedback\n * Uses singleton HapticController instance\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * // Light haptic for UI interaction\n * triggerOptimizedHaptic('light');\n * \n * // Medium haptic for combat action\n * triggerOptimizedHaptic('medium');\n * \n * // Strong haptic for critical hit\n * triggerOptimizedHaptic('strong');\n * ```\n * \n * @public\n */\nexport function triggerOptimizedHaptic(intensity: HapticIntensity): boolean {\n return HapticController.getInstance().trigger(intensity);\n}\n\n/**\n * Convenience function to trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @public\n */\nexport function triggerCustomOptimizedHaptic(pattern: number | number[]): boolean {\n return HapticController.getInstance().triggerCustom(pattern);\n}\n\n/**\n * Convenience function to stop haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * @public\n */\nexport function stopOptimizedHaptic(): boolean {\n return HapticController.getInstance().stop();\n}\n\n/**\n * Combat-specific optimized haptic patterns\n * Pre-configured for common combat scenarios\n * \n * @korean 전투 햅틱 패턴\n * @public\n */\nexport const OptimizedCombatHaptics = {\n /**\n * Standard attack hit feedback\n * @korean 일반 공격\n */\n attack: () => triggerOptimizedHaptic('medium'),\n\n /**\n * Block successful feedback\n * @korean 방어 성공\n */\n block: () => triggerOptimizedHaptic('light'),\n\n /**\n * Critical hit feedback with double pulse\n * @korean 크리티컬 히트\n */\n criticalHit: () => triggerCustomOptimizedHaptic([40, 20, 60]),\n\n /**\n * Vital point strike feedback\n * @korean 급소 타격\n */\n vitalPointStrike: () => triggerOptimizedHaptic('strong'),\n\n /**\n * Stance change feedback\n * @korean 자세 변경\n */\n stanceChange: () => triggerOptimizedHaptic('light'),\n\n /**\n * Combo counter increment\n * @korean 콤보 카운터\n */\n comboIncrement: () => triggerOptimizedHaptic('light'),\n\n /**\n * Player KO feedback with extended pattern\n * @korean 플레이어 KO\n */\n knockout: () => triggerCustomOptimizedHaptic([60, 30, 60, 30, 100]),\n\n /**\n * Error or invalid action feedback\n * @korean 오류 피드백\n */\n error: () => triggerCustomOptimizedHaptic([15, 10, 15]),\n} as const;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MobileControlsPure.js","names":[],"sources":["../../../../src/components/shared/mobile/MobileControlsPure.tsx"],"sourcesContent":["/**\n * MobileControlsPure - Pure DOM mobile controls (no Three.js/drei dependency)\n *\n * These controls render OUTSIDE the Three.js Canvas for reliable touch event handling.\n * The key difference from VirtualDPad/ActionButtons is that these don't use drei's Html\n * component, which can intercept touch events on mobile devices.\n *\n * Visual style matches the existing Korean cyberpunk aesthetic.\n *\n * @module components/mobile/MobileControlsPure\n * @category Mobile Controls\n * @korean 순수 DOM 모바일 컨트롤\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\n\n// Re-export types from VirtualDPad for compatibility\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\nexport type DPadEventType = \"start\" | \"end\";\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for the combined mobile controls overlay\n */\nexport interface MobileControlsOverlayProps {\n /** Callback when D-Pad direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Callback when attack is pressed */\n readonly onAttack: () => void;\n /** Callback when block is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether controls are disabled */\n readonly disabled?: boolean;\n /** Bottom offset in pixels (default: 160 to clear BottomHUD) */\n readonly bottom?: number;\n /** Opacity (default: 0.85) */\n readonly opacity?: number;\n /** Viewport width for responsive control sizing */\n readonly viewportWidth?: number;\n /** Viewport height for responsive control sizing */\n readonly viewportHeight?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number;\n readonly symbol: string;\n readonly keys: string[]; // Keys to dispatch\n}\n\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, symbol: \"▲\", keys: [\"w\"] },\n { direction: \"up-right\", angle: 45, symbol: \"◥\", keys: [\"w\", \"d\"] },\n { direction: \"right\", angle: 90, symbol: \"▶\", keys: [\"d\"] },\n { direction: \"down-right\", angle: 135, symbol: \"◢\", keys: [\"s\", \"d\"] },\n { direction: \"down\", angle: 180, symbol: \"▼\", keys: [\"s\"] },\n { direction: \"down-left\", angle: 225, symbol: \"◣\", keys: [\"s\", \"a\"] },\n { direction: \"left\", angle: 270, symbol: \"◀\", keys: [\"a\"] },\n { direction: \"up-left\", angle: 315, symbol: \"◤\", keys: [\"w\", \"a\"] },\n] as const;\n\n/**\n * Fallback CSS-pixel viewport used only when a parent does not provide live\n * dimensions. 390×844 matches the common iPhone 13/14/15 CSS viewport class\n * and approximates many mid-size Android portrait viewports, avoiding oversized\n * controls on compact devices.\n */\nconst DEFAULT_MOBILE_VIEWPORT = {\n width: 390,\n height: 844,\n} as const;\n\n/**\n * D-Pad diameter target as a ratio of the shortest viewport side. 34% keeps the\n * full radial control reachable by thumb while each directional button remains\n * at or above the 44px WCAG touch target minimum after clamping.\n */\nconst DPAD_SHORTEST_SIDE_RATIO = 0.34;\n\n/**\n * MobileControlsOverlay - Floating mobile controls rendered outside Canvas\n *\n * Positions D-Pad on left, Action buttons on right, floating above BottomHUD.\n * Uses pure DOM events for reliable mobile touch handling.\n */\nexport const MobileControlsOverlay: React.FC<MobileControlsOverlayProps> =\n React.memo(\n ({\n onMove,\n onAttack,\n onBlock,\n disabled = false,\n bottom = 160,\n opacity = 0.85,\n viewportWidth = DEFAULT_MOBILE_VIEWPORT.width,\n viewportHeight = DEFAULT_MOBILE_VIEWPORT.height,\n }) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n\n // D-Pad/action sizing scales down on narrow mobile screens while\n // preserving WCAG touch target minimums.\n const controlLayout = useMemo(() => {\n const shortestSide = Math.min(viewportWidth, viewportHeight);\n const dpadSize = Math.round(\n Math.max(\n 112,\n Math.min(140, shortestSide * DPAD_SHORTEST_SIDE_RATIO),\n ),\n );\n const buttonSize = Math.max(44, Math.round(dpadSize * 0.34));\n const buttonPlacementRadius = dpadSize * 0.32;\n const attackSize = Math.round(\n Math.max(64, Math.min(80, dpadSize * 0.58)),\n );\n const blockSize = Math.round(\n Math.max(54, Math.min(65, dpadSize * 0.47)),\n );\n const sidePadding = Math.round(\n Math.max(12, Math.min(20, viewportWidth * 0.04)),\n );\n\n return {\n dpadSize,\n buttonSize,\n buttonPlacementRadius,\n attackSize,\n blockSize,\n sidePadding,\n overlayHeight: dpadSize + 32,\n actionGap: Math.max(8, Math.round(dpadSize * 0.08)),\n };\n }, [viewportHeight, viewportWidth]);\n\n // Handle D-Pad press\n const handleDPadStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(direction);\n triggerHaptic(\"light\");\n onMove(direction, \"start\");\n },\n [disabled, onMove],\n );\n\n // Handle D-Pad release\n const handleDPadEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(null);\n onMove(null, \"end\");\n },\n [disabled, onMove],\n );\n\n // Handle Attack press\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(true);\n triggerHaptic(\"medium\");\n onAttack();\n },\n [disabled, onAttack],\n );\n\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(false);\n },\n [],\n );\n\n // Handle Block press/release\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(true);\n triggerHaptic(\"light\");\n onBlock(\"start\");\n },\n [disabled, onBlock],\n );\n\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(false);\n onBlock(\"end\");\n },\n [onBlock],\n );\n\n // Common button styles\n const glowStyle = useMemo(\n () => ({\n boxShadow: `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}, inset 0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }),\n [],\n );\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${bottom}px`,\n left: 0,\n right: 0,\n height: `${controlLayout.overlayHeight}px`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"flex-end\",\n padding: `0 ${controlLayout.sidePadding}px`,\n pointerEvents: \"none\",\n zIndex: 1000,\n opacity: disabled ? 0.4 : opacity,\n }}\n data-testid=\"mobile-controls-overlay\"\n >\n {/* D-Pad (Left Side) */}\n <div\n style={{\n position: \"relative\",\n width: `${controlLayout.dpadSize}px`,\n height: `${controlLayout.dpadSize}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-dpad\"\n >\n {/* D-Pad Background Circle */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: `${controlLayout.dpadSize * 0.9}px`,\n height: `${controlLayout.dpadSize * 0.9}px`,\n borderRadius: \"50%\",\n background: `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95)} 100%)`,\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.5)}`,\n ...glowStyle,\n }}\n />\n\n {/* Direction Buttons */}\n {DIRECTIONS.map((config) => {\n const radian = (config.angle - 90) * (Math.PI / 180);\n const x = Math.cos(radian) * controlLayout.buttonPlacementRadius;\n const y = Math.sin(radian) * controlLayout.buttonPlacementRadius;\n const isActive = activeDirection === config.direction;\n\n return (\n <button\n key={config.direction}\n onTouchStart={(e) => handleDPadStart(e, config.direction)}\n onTouchEnd={handleDPadEnd}\n onTouchCancel={handleDPadEnd}\n onMouseDown={(e) => handleDPadStart(e, config.direction)}\n onMouseUp={handleDPadEnd}\n onMouseLeave={handleDPadEnd}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${controlLayout.buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${controlLayout.buttonSize / 2}px)`,\n width: `${controlLayout.buttonSize}px`,\n height: `${controlLayout.buttonSize}px`,\n borderRadius: \"50%\",\n background: isActive\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 100%)`,\n border: `2px solid ${hexToRgbaString(isActive ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.PRIMARY_CYAN, isActive ? 1 : 0.7)}`,\n fontSize: \"14px\",\n color: isActive\n ? hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: isActive ? \"scale(1.15)\" : \"scale(1)\",\n transition: \"transform 0.1s ease, background 0.1s ease\",\n boxShadow: isActive\n ? `0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8)}`\n : \"none\",\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n }}\n aria-label={`이동 ${config.direction} | Move ${config.direction}`}\n data-testid={`mobile-dpad-${config.direction}`}\n >\n {config.symbol}\n </button>\n );\n })}\n\n {/* Center Indicator */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"20px\",\n height: \"20px\",\n borderRadius: \"50%\",\n background: activeDirection\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)\n : hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.8)}`,\n transition: \"background 0.15s ease\",\n }}\n />\n </div>\n\n {/* Action Buttons (Right Side) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${controlLayout.actionGap}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-action-buttons\"\n >\n {/* Attack Button - Large Red */}\n <button\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onTouchCancel={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n style={{\n width: `${controlLayout.attackSize}px`,\n height: `${controlLayout.attackSize}px`,\n borderRadius: \"50%\",\n background: attackPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.NEGATIVE_RED_DARK, 0.9)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)}`,\n fontSize: \"28px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: attackPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: attackPressed\n ? `0 0 25px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)}`\n : `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.5)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"공격 | Attack\"\n data-testid=\"mobile-attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button - Smaller Cyan */}\n <button\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onTouchCancel={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n style={{\n width: `${controlLayout.blockSize}px`,\n height: `${controlLayout.blockSize}px`,\n borderRadius: \"50%\",\n marginLeft: \"auto\",\n background: blockPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.KI_LOW, 0.8)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n fontSize: \"22px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: blockPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: blockPressed\n ? `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9)}`\n : `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"방어 | Block\"\n data-testid=\"mobile-block-button\"\n >\n 🛡️\n </button>\n </div>\n </div>\n );\n },\n );\n\nMobileControlsOverlay.displayName = \"MobileControlsOverlay\";\n\nexport default MobileControlsOverlay;\n"],"mappings":"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"}
1
+ {"version":3,"file":"MobileControlsPure.js","names":[],"sources":["../../../../src/components/shared/mobile/MobileControlsPure.tsx"],"sourcesContent":["/**\n * MobileControlsPure - Pure DOM mobile controls (no Three.js/drei dependency)\n *\n * These controls render OUTSIDE the Three.js Canvas for reliable touch event handling.\n * The key difference from VirtualDPad/ActionButtons is that these don't use drei's Html\n * component, which can intercept touch events on mobile devices.\n *\n * Visual style matches the existing Korean cyberpunk aesthetic.\n *\n * @module components/mobile/MobileControlsPure\n * @category Mobile Controls\n * @korean 순수 DOM 모바일 컨트롤\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\n\n// Re-export types from VirtualDPad for compatibility\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\nexport type DPadEventType = \"start\" | \"end\";\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for the combined mobile controls overlay\n */\nexport interface MobileControlsOverlayProps {\n /** Callback when D-Pad direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Callback when attack is pressed */\n readonly onAttack: () => void;\n /** Callback when block is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether controls are disabled */\n readonly disabled?: boolean;\n /** Bottom offset in pixels (default: 160 to clear BottomHUD) */\n readonly bottom?: number;\n /** Opacity (default: 0.85) */\n readonly opacity?: number;\n /** Viewport width for responsive control sizing */\n readonly viewportWidth?: number;\n /** Viewport height for responsive control sizing */\n readonly viewportHeight?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number;\n readonly symbol: string;\n readonly keys: string[]; // Keys to dispatch\n}\n\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, symbol: \"▲\", keys: [\"w\"] },\n { direction: \"up-right\", angle: 45, symbol: \"◥\", keys: [\"w\", \"d\"] },\n { direction: \"right\", angle: 90, symbol: \"▶\", keys: [\"d\"] },\n { direction: \"down-right\", angle: 135, symbol: \"◢\", keys: [\"s\", \"d\"] },\n { direction: \"down\", angle: 180, symbol: \"▼\", keys: [\"s\"] },\n { direction: \"down-left\", angle: 225, symbol: \"◣\", keys: [\"s\", \"a\"] },\n { direction: \"left\", angle: 270, symbol: \"◀\", keys: [\"a\"] },\n { direction: \"up-left\", angle: 315, symbol: \"◤\", keys: [\"w\", \"a\"] },\n] as const;\n\n/**\n * Fallback CSS-pixel viewport used only when a parent does not provide live\n * dimensions. 390×844 matches the common iPhone 13/14/15 CSS viewport class\n * and approximates many mid-size Android portrait viewports, avoiding oversized\n * controls on compact devices.\n */\nconst DEFAULT_MOBILE_VIEWPORT = {\n width: 390,\n height: 844,\n} as const;\n\n/**\n * D-Pad diameter target as a ratio of the shortest viewport side. 34% keeps the\n * full radial control reachable by thumb while each directional button remains\n * at or above the 44px WCAG touch target minimum after clamping.\n */\nconst DPAD_SHORTEST_SIDE_RATIO = 0.34;\n\n/**\n * MobileControlsOverlay - Floating mobile controls rendered outside Canvas\n *\n * Positions D-Pad on left, Action buttons on right, floating above BottomHUD.\n * Uses pure DOM events for reliable mobile touch handling.\n */\nexport const MobileControlsOverlay: React.FC<MobileControlsOverlayProps> =\n React.memo(\n ({\n onMove,\n onAttack,\n onBlock,\n disabled = false,\n bottom = 160,\n opacity = 0.85,\n viewportWidth = DEFAULT_MOBILE_VIEWPORT.width,\n viewportHeight = DEFAULT_MOBILE_VIEWPORT.height,\n }) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n\n // D-Pad/action sizing scales down on narrow mobile screens while\n // preserving WCAG touch target minimums.\n const controlLayout = useMemo(() => {\n const shortestSide = Math.min(viewportWidth, viewportHeight);\n const dpadSize = Math.round(\n Math.max(\n 112,\n Math.min(140, shortestSide * DPAD_SHORTEST_SIDE_RATIO),\n ),\n );\n const buttonSize = Math.max(44, Math.round(dpadSize * 0.34));\n const buttonPlacementRadius = dpadSize * 0.32;\n const attackSize = Math.round(\n Math.max(64, Math.min(80, dpadSize * 0.58)),\n );\n const blockSize = Math.round(\n Math.max(54, Math.min(65, dpadSize * 0.47)),\n );\n const sidePadding = Math.round(\n Math.max(12, Math.min(20, viewportWidth * 0.04)),\n );\n\n return {\n dpadSize,\n buttonSize,\n buttonPlacementRadius,\n attackSize,\n blockSize,\n sidePadding,\n overlayHeight: dpadSize + 32,\n actionGap: Math.max(8, Math.round(dpadSize * 0.08)),\n };\n }, [viewportHeight, viewportWidth]);\n\n // Handle D-Pad press\n const handleDPadStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(direction);\n triggerHaptic(\"light\");\n onMove(direction, \"start\");\n },\n [disabled, onMove],\n );\n\n // Handle D-Pad release\n const handleDPadEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(null);\n onMove(null, \"end\");\n },\n [disabled, onMove],\n );\n\n // Handle Attack press\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(true);\n triggerHaptic(\"medium\");\n onAttack();\n },\n [disabled, onAttack],\n );\n\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(false);\n },\n [],\n );\n\n // Handle Block press/release\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(true);\n triggerHaptic(\"light\");\n onBlock(\"start\");\n },\n [disabled, onBlock],\n );\n\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(false);\n onBlock(\"end\");\n },\n [onBlock],\n );\n\n // Common button styles\n const glowStyle = useMemo(\n () => ({\n boxShadow: `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}, inset 0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }),\n [],\n );\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${bottom}px`,\n left: 0,\n right: 0,\n height: `${controlLayout.overlayHeight}px`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"flex-end\",\n padding: `0 ${controlLayout.sidePadding}px`,\n pointerEvents: \"none\",\n zIndex: 1000,\n opacity: disabled ? 0.4 : opacity,\n }}\n data-testid=\"mobile-controls-overlay\"\n >\n {/* D-Pad (Left Side) */}\n <div\n style={{\n position: \"relative\",\n width: `${controlLayout.dpadSize}px`,\n height: `${controlLayout.dpadSize}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-dpad\"\n >\n {/* D-Pad Background Circle */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: `${controlLayout.dpadSize * 0.9}px`,\n height: `${controlLayout.dpadSize * 0.9}px`,\n borderRadius: \"50%\",\n background: `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95)} 100%)`,\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.5)}`,\n ...glowStyle,\n }}\n />\n\n {/* Direction Buttons */}\n {DIRECTIONS.map((config) => {\n const radian = (config.angle - 90) * (Math.PI / 180);\n const x = Math.cos(radian) * controlLayout.buttonPlacementRadius;\n const y = Math.sin(radian) * controlLayout.buttonPlacementRadius;\n const isActive = activeDirection === config.direction;\n\n return (\n <button\n key={config.direction}\n onTouchStart={(e) => handleDPadStart(e, config.direction)}\n onTouchEnd={handleDPadEnd}\n onTouchCancel={handleDPadEnd}\n onMouseDown={(e) => handleDPadStart(e, config.direction)}\n onMouseUp={handleDPadEnd}\n onMouseLeave={handleDPadEnd}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${controlLayout.buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${controlLayout.buttonSize / 2}px)`,\n width: `${controlLayout.buttonSize}px`,\n height: `${controlLayout.buttonSize}px`,\n borderRadius: \"50%\",\n background: isActive\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 100%)`,\n border: `2px solid ${hexToRgbaString(isActive ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.PRIMARY_CYAN, isActive ? 1 : 0.7)}`,\n fontSize: \"14px\",\n color: isActive\n ? hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: isActive ? \"scale(1.15)\" : \"scale(1)\",\n transition: \"transform 0.1s ease, background 0.1s ease\",\n boxShadow: isActive\n ? `0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8)}`\n : \"none\",\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n }}\n aria-label={`이동 ${config.direction} | Move ${config.direction}`}\n data-testid={`mobile-dpad-${config.direction}`}\n >\n {config.symbol}\n </button>\n );\n })}\n\n {/* Center Indicator */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"20px\",\n height: \"20px\",\n borderRadius: \"50%\",\n background: activeDirection\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)\n : hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.8)}`,\n transition: \"background 0.15s ease\",\n }}\n />\n </div>\n\n {/* Action Buttons (Right Side) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${controlLayout.actionGap}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-action-buttons\"\n >\n {/* Attack Button - Large Red */}\n <button\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onTouchCancel={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n style={{\n width: `${controlLayout.attackSize}px`,\n height: `${controlLayout.attackSize}px`,\n borderRadius: \"50%\",\n background: attackPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.NEGATIVE_RED_DARK, 0.9)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)}`,\n fontSize: \"28px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: attackPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: attackPressed\n ? `0 0 25px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)}`\n : `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.5)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"공격 | Attack\"\n data-testid=\"mobile-attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button - Smaller Cyan */}\n <button\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onTouchCancel={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n style={{\n width: `${controlLayout.blockSize}px`,\n height: `${controlLayout.blockSize}px`,\n borderRadius: \"50%\",\n marginLeft: \"auto\",\n background: blockPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.KI_LOW, 0.8)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n fontSize: \"22px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: blockPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: blockPressed\n ? `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9)}`\n : `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"방어 | Block\"\n data-testid=\"mobile-block-button\"\n >\n 🛡️\n </button>\n </div>\n </div>\n );\n },\n );\n\nMobileControlsOverlay.displayName = \"MobileControlsOverlay\";\n\nexport default MobileControlsOverlay;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StanceWheelPure.js","names":[],"sources":["../../../../src/components/shared/mobile/StanceWheelPure.tsx"],"sourcesContent":["/**\n * StanceWheelPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Circular 8-segment stance selector for mobile touch controls\n * Provides visual and tactile stance switching interface\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 stance buttons\n * - Keyboard navigation (Tab, Enter, Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - aria-expanded state for wheel toggle\n * - role=\"radiogroup\" for stance selection\n * - 50x50px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/StanceWheelPure\n * @category Mobile Controls\n * @korean 자세 휠 (순수 DOM)\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../types/common\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport {\n handleKeyboardNav,\n getFocusStyle,\n announceToScreenReader,\n} from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\n\n/**\n * Props for StanceWheelPure component\n */\nexport interface StanceWheelPureProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Callback when stance changes */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Whether wheel is expanded */\n readonly expanded: boolean;\n /** Callback to toggle expansion */\n readonly onToggle: () => void;\n /** Whether wheel is disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 when collapsed, 100 when expanded for safe area) */\n readonly bottom?: number;\n /** Opacity of wheel (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Trigram symbols for each stance\n */\nconst TRIGRAM_SYMBOLS = [\n \"☰\",\n \"☱\",\n \"☲\",\n \"☳\",\n \"☴\",\n \"☵\",\n \"☶\",\n \"☷\",\n] as const;\n\n/**\n * Korean names for each stance\n */\nconst STANCE_KOREAN_NAMES = [\n \"건\", // Geon - Heaven\n \"태\", // Tae - Lake\n \"리\", // Li - Fire\n \"진\", // Jin - Thunder\n \"손\", // Son - Wind\n \"감\", // Gam - Water\n \"간\", // Gan - Mountain\n \"곤\", // Gon - Earth\n] as const;\n\n/**\n * Get color for a specific stance\n */\nconst getStanceColor = (stance: TrigramStance): number => {\n const stanceColors: Record<TrigramStance, number> = {\n geon: 0xffd700, // Gold - Heaven\n tae: 0x00ffff, // Cyan - Lake\n li: 0xff4444, // Red - Fire\n jin: 0xffaa00, // Orange - Thunder\n son: 0x88ff88, // Light Green - Wind\n gam: 0x0088ff, // Blue - Water\n gan: 0x8844ff, // Purple - Mountain\n gon: 0xaa6644, // Brown - Earth\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * StanceWheelPure Component\n *\n * Pure DOM circular stance selector with 8 segments for trigram stances\n * Features:\n * - Expandable/collapsible interface\n * - Visual stance indicator when collapsed (60x60px)\n * - 8 touch-optimized stance buttons when expanded (50x50px each)\n * - 200px wheel diameter with safe positioning\n * - Korean trigram symbols and names\n * - Color-coded by stance element\n * - Haptic feedback on selection\n * - 50x50px minimum touch targets\n *\n * Usage in Combat:\n * - Tap collapsed indicator to expand wheel\n * - Select from 8 trigram stances\n * - Current stance highlighted with gold accent\n * - Tap current stance to collapse wheel\n *\n * @example\n * ```tsx\n * <StanceWheelPure\n * currentStance={player.stance}\n * onStanceChange={(index) => handleStanceChange(index)}\n * expanded={wheelExpanded}\n * onToggle={() => setWheelExpanded(!wheelExpanded)}\n * disabled={isPaused}\n * />\n * ```\n *\n * @public\n * @korean 자세휠순수\n */\nexport const StanceWheelPure: React.FC<StanceWheelPureProps> = ({\n currentStance,\n onStanceChange,\n expanded,\n onToggle,\n disabled = false,\n bottom,\n opacity = 0.8,\n}) => {\n const [hoveredStance, setHoveredStance] = useState<number | null>(null);\n const [focusedStance, setFocusedStance] = useState<number | null>(null);\n\n /**\n * Handle stance selection (touch or mouse)\n */\n const handleStanceSelect = useCallback(\n (e: React.TouchEvent | React.MouseEvent, stanceIndex: number) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Don't allow selecting the same stance\n if (stanceIndex === currentStance) {\n // Collapse wheel if tapping current stance\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\n // Announce stance change to screen readers\n const stanceName = STANCE_KOREAN_NAMES[stanceIndex];\n const stanceSymbol = TRIGRAM_SYMBOLS[stanceIndex];\n announceToScreenReader({\n message: createBilingualLabel(\n `자세 변경: ${stanceName} ${stanceSymbol}`,\n `Stance changed to ${TRIGRAM_STANCES_ORDER[stanceIndex]}`,\n ).label,\n politeness: \"polite\",\n });\n\n // Auto-collapse after selection (optional)\n // onToggle();\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle wheel toggle (touch or mouse)\n */\n const handleToggle = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n onToggle();\n triggerHaptic(\"light\");\n\n // Announce state change to screen readers using the toggled value\n const nextExpanded = !expanded;\n announceToScreenReader({\n message: createBilingualLabel(\n nextExpanded ? \"자세 휠 열림\" : \"자세 휠 닫힘\",\n nextExpanded ? \"Stance wheel opened\" : \"Stance wheel closed\",\n ).label,\n politeness: \"polite\",\n });\n },\n [disabled, onToggle, expanded],\n );\n\n /**\n * Handle keyboard navigation for stance buttons\n */\n const handleStanceKeyDown = useCallback(\n (stanceIndex: number) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n if (stanceIndex === currentStance) {\n onToggle();\n } else {\n onStanceChange(stanceIndex);\n }\n triggerHaptic(\"medium\");\n },\n onCancel: () => {\n onToggle();\n },\n onNavigate: (direction) => {\n // Navigate between stances with arrow keys\n let newIndex = stanceIndex;\n if (direction === \"left\" || direction === \"up\") {\n newIndex = (stanceIndex - 1 + 8) % 8;\n } else if (direction === \"right\" || direction === \"down\") {\n newIndex = (stanceIndex + 1) % 8;\n }\n setFocusedStance(newIndex);\n // Focus the new stance button on the next animation frame\n requestAnimationFrame(() => {\n const button = document.querySelector(\n `[data-testid=\"stance-button-pure-${newIndex}\"]`,\n ) as HTMLElement | null;\n button?.focus();\n });\n },\n });\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle keyboard navigation for toggle button\n */\n const handleToggleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n onToggle();\n triggerHaptic(\"light\");\n },\n });\n },\n [disabled, onToggle],\n );\n\n // Dynamic bottom position with safe area consideration\n const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\n // Get RGB values for colors using shared utility\n const currentStanceColor = getColorRGB(\n getStanceColor(TRIGRAM_STANCES_ORDER[currentStance]),\n );\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n if (expanded) {\n // Expanded: Show full 8-segment wheel\n const wheelSize = 200;\n const segmentAngle = 360 / 8;\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-expanded\"\n role=\"radiogroup\"\n aria-label={\n createBilingualLabel(\n \"팔괘 자세 선택\",\n \"Eight Trigram Stance Selection\",\n ).label\n }\n aria-activedescendant={`stance-button-pure-${currentStance}`}\n >\n <div\n style={{\n width: `${wheelSize}px`,\n height: `${wheelSize}px`,\n position: \"relative\",\n }}\n >\n {/* Stance buttons arranged in circle */}\n {TRIGRAM_STANCES_ORDER.map((stance, index) => {\n const angle = index * segmentAngle;\n const radian = (angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * 80 + 100;\n const y = Math.sin(radian) * 80 + 100;\n const isActive = index === currentStance;\n const isHovered = index === hoveredStance;\n const stanceColor = getColorRGB(getStanceColor(stance));\n\n return (\n <button\n key={stance}\n onTouchStart={(e) => handleStanceSelect(e, index)}\n onTouchEnd={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setHoveredStance(null);\n }}\n onMouseDown={(e) => handleStanceSelect(e, index)}\n onMouseEnter={() => setHoveredStance(index)}\n onMouseLeave={() => setHoveredStance(null)}\n onKeyDown={handleStanceKeyDown(index)}\n onFocus={() => setFocusedStance(index)}\n onBlur={() => setFocusedStance(null)}\n style={{\n position: \"absolute\",\n left: `${x - 25}px`,\n top: `${y - 25}px`,\n width: \"50px\",\n height: \"50px\",\n borderRadius: \"50%\",\n background: isActive\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.95)`\n : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`,\n border: `3px solid ${isActive ? \"#fff\" : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 1)`}`,\n fontSize: \"24px\",\n color: isActive ? \"#000\" : \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n transform: isActive || isHovered ? \"scale(1.15)\" : \"scale(1)\",\n boxShadow: isActive\n ? `0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : isHovered\n ? `0 0 15px rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`\n : `0 4px 10px rgba(0, 0, 0, 0.5)`,\n ...getFocusStyle(focusedStance === index, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n aria-label={\n createBilingualLabel(\n `${STANCE_KOREAN_NAMES[index]} ${TRIGRAM_SYMBOLS[index]}`,\n `${stance} stance`,\n ).label\n }\n aria-checked={isActive}\n role=\"radio\"\n tabIndex={0}\n disabled={disabled}\n id={`stance-button-pure-${index}`}\n data-testid={`stance-button-pure-${index}`}\n >\n <div style={{ fontSize: \"20px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[index]}\n </div>\n <div style={{ fontSize: \"8px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[index]}\n </div>\n </button>\n );\n })}\n\n {/* Center label */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontSize: \"12px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n textAlign: \"center\",\n pointerEvents: \"none\",\n }}\n >\n 자세 선택\n <br />\n <span style={{ fontSize: \"10px\" }}>Stance</span>\n </div>\n </div>\n </div>\n );\n }\n\n // Collapsed: Show current stance indicator\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-collapsed\"\n >\n <button\n onTouchStart={handleToggle}\n onMouseDown={handleToggle}\n onKeyDown={handleToggleKeyDown}\n onFocus={() => setFocusedStance(currentStance)}\n onBlur={() => setFocusedStance(null)}\n style={{\n width: \"60px\",\n height: \"60px\",\n borderRadius: \"50%\",\n background: `rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.9)`,\n border: `3px solid rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n fontSize: \"28px\",\n color: \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n ...getFocusStyle(focusedStance === currentStance, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n disabled={disabled}\n aria-label={\n createBilingualLabel(\n `현재 자세: ${STANCE_KOREAN_NAMES[currentStance]} ${TRIGRAM_SYMBOLS[currentStance]}. 자세 휠 열기`,\n `Current stance: ${TRIGRAM_STANCES_ORDER[currentStance]}. Open stance wheel`,\n ).label\n }\n aria-expanded={expanded}\n aria-haspopup=\"menu\"\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"stance-wheel-pure-toggle\"\n >\n <div style={{ fontSize: \"24px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[currentStance]}\n </div>\n <div style={{ fontSize: \"10px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[currentStance]}\n </div>\n </button>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,IAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;AAKD,IAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;AAKD,IAAM,kBAAkB,WAAkC;AAWxD,QAAO;EATL,MAAM;EACN,KAAK;EACL,IAAI;EACJ,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EAEA,CAAa,WAAW,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqC/C,IAAa,mBAAmD,EAC9D,eACA,gBACA,UACA,UACA,WAAW,OACX,QACA,UAAU,SACN;CACJ,MAAM,CAAC,eAAe,oBAAoB,SAAwB,KAAK;CACvE,MAAM,CAAC,eAAe,oBAAoB,SAAwB,KAAK;;;;CAKvE,MAAM,qBAAqB,aACxB,GAAwC,gBAAwB;AAC/D,MAAI,SAAU;AACd,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAGnB,MAAI,gBAAgB,eAAe;AAEjC,aAAU;AACV,iBAAc,QAAQ;AACtB;;AAGF,iBAAe,YAAY;AAC3B,gBAAc,SAAS;EAGvB,MAAM,aAAa,oBAAoB;EACvC,MAAM,eAAe,gBAAgB;AACrC,yBAAuB;GACrB,SAAS,qBACP,UAAU,WAAW,GAAG,gBACxB,qBAAqB,sBAAsB,eAC5C,CAAC;GACF,YAAY;GACb,CAAC;IAKJ;EAAC;EAAU;EAAe;EAAgB;EAAS,CACpD;;;;CAKD,MAAM,eAAe,aAClB,MAA2C;AAC1C,MAAI,SAAU;AACd,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,YAAU;AACV,gBAAc,QAAQ;EAGtB,MAAM,eAAe,CAAC;AACtB,yBAAuB;GACrB,SAAS,qBACP,eAAe,YAAY,WAC3B,eAAe,wBAAwB,sBACxC,CAAC;GACF,YAAY;GACb,CAAC;IAEJ;EAAC;EAAU;EAAU;EAAS,CAC/B;;;;CAKD,MAAM,sBAAsB,aACzB,iBAAyB,MAA2B;AACnD,MAAI,SAAU;AACd,oBAAkB,EAAE,aAAa;GAC/B,kBAAkB;AAChB,QAAI,gBAAgB,cAClB,WAAU;QAEV,gBAAe,YAAY;AAE7B,kBAAc,SAAS;;GAEzB,gBAAgB;AACd,cAAU;;GAEZ,aAAa,cAAc;IAEzB,IAAI,WAAW;AACf,QAAI,cAAc,UAAU,cAAc,KACxC,aAAY,cAAc,IAAI,KAAK;aAC1B,cAAc,WAAW,cAAc,OAChD,aAAY,cAAc,KAAK;AAEjC,qBAAiB,SAAS;AAE1B,gCAA4B;AACX,cAAS,cACtB,oCAAoC,SAAS,IAE/C,EAAQ,OAAO;MACf;;GAEL,CAAC;IAEJ;EAAC;EAAU;EAAe;EAAgB;EAAS,CACpD;;;;CAKD,MAAM,sBAAsB,aACzB,MAA2B;AAC1B,MAAI,SAAU;AACd,oBAAkB,EAAE,aAAa,EAC/B,kBAAkB;AAChB,aAAU;AACV,iBAAc,QAAQ;KAEzB,CAAC;IAEJ,CAAC,UAAU,SAAS,CACrB;CAGD,MAAM,gBAAgB,WAAW,WAAW,MAAM;CAGlD,MAAM,qBAAqB,YACzB,eAAe,sBAAsB,eAAe,CACrD;CACD,MAAM,YAAY,YAAY,cAAc,YAAY;CACxD,MAAM,eAAe,YAAY,cAAc,aAAa;AAE5D,KAAI,UAAU;EAEZ,MAAM,YAAY;EAClB,MAAM,eAAe,MAAM;AAE3B,SACE,oBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,QAAQ,GAAG,cAAc;IACzB,MAAM;IACN,WAAW;IACX,SAAS,WAAW,KAAM;IAC1B,eAAe,WAAW,SAAS;IACnC,YAAY;IACZ,QAAQ;IACT;GACD,eAAY;GACZ,MAAK;GACL,cACE,qBACE,YACA,iCACD,CAAC;GAEJ,yBAAuB,sBAAsB;aAE7C,qBAAC,OAAD;IACE,OAAO;KACL,OAAO,GAAG,UAAU;KACpB,QAAQ,GAAG,UAAU;KACrB,UAAU;KACX;cALH,CAQG,sBAAsB,KAAK,QAAQ,UAAU;KAE5C,MAAM,UADQ,QAAQ,eACE,OAAO,KAAK,KAAK;KACzC,MAAM,IAAI,KAAK,IAAI,OAAO,GAAG,KAAK;KAClC,MAAM,IAAI,KAAK,IAAI,OAAO,GAAG,KAAK;KAClC,MAAM,WAAW,UAAU;KAC3B,MAAM,YAAY,UAAU;KAC5B,MAAM,cAAc,YAAY,eAAe,OAAO,CAAC;AAEvD,YACE,qBAAC,UAAD;MAEE,eAAe,MAAM,mBAAmB,GAAG,MAAM;MACjD,aAAa,MAAM;AACjB,SAAE,gBAAgB;AAClB,SAAE,iBAAiB;AACnB,wBAAiB,KAAK;;MAExB,cAAc,MAAM,mBAAmB,GAAG,MAAM;MAChD,oBAAoB,iBAAiB,MAAM;MAC3C,oBAAoB,iBAAiB,KAAK;MAC1C,WAAW,oBAAoB,MAAM;MACrC,eAAe,iBAAiB,MAAM;MACtC,cAAc,iBAAiB,KAAK;MACpC,OAAO;OACL,UAAU;OACV,MAAM,GAAG,IAAI,GAAG;OAChB,KAAK,GAAG,IAAI,GAAG;OACf,OAAO;OACP,QAAQ;OACR,cAAc;OACd,YAAY,WACR,QAAQ,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,WACpD,QAAQ,YAAY,EAAE,IAAI,YAAY,EAAE,IAAI,YAAY,EAAE;OAC9D,QAAQ,aAAa,WAAW,SAAS,QAAQ,YAAY,EAAE,IAAI,YAAY,EAAE,IAAI,YAAY,EAAE;OACnG,UAAU;OACV,OAAO,WAAW,SAAS;OAC3B,YAAY;OACZ,SAAS;OACT,eAAe;OACf,YAAY;OACZ,gBAAgB;OAChB,QAAQ;OACR,YAAY;OACZ,aAAa;OACb,YAAY;OACZ,WAAW,YAAY,YAAY,gBAAgB;OACnD,WAAW,WACP,iBAAiB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,UAC7D,YACE,iBAAiB,YAAY,EAAE,IAAI,YAAY,EAAE,IAAI,YAAY,EAAE,UACnE;OACN,GAAG,cAAc,kBAAkB,OAAO;QACxC,WAAW,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,wBAAwB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE;QACtJ,cAAc,cAAc;QAC5B,cAAc;QACf,CAAC;OACH;MACD,cACE,qBACE,GAAG,oBAAoB,OAAO,GAAG,gBAAgB,UACjD,GAAG,OAAO,SACX,CAAC;MAEJ,gBAAc;MACd,MAAK;MACL,UAAU;MACA;MACV,IAAI,sBAAsB;MAC1B,eAAa,sBAAsB;gBA3DrC,CA6DE,oBAAC,OAAD;OAAK,OAAO;QAAE,UAAU;QAAQ,YAAY;QAAG;iBAC5C,gBAAgB;OACb,CAAA,EACN,oBAAC,OAAD;OAAK,OAAO;QAAE,UAAU;QAAO,WAAW;QAAO;iBAC9C,oBAAoB;OACjB,CAAA,CACC;QAlEF,OAkEE;MAEX,EAGF,qBAAC,OAAD;KACE,OAAO;MACL,UAAU;MACV,KAAK;MACL,MAAM;MACN,WAAW;MACX,UAAU;MACV,OAAO,QAAQ,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE;MACpE,YAAY;MACZ,YAAY;MACZ,WAAW;MACX,eAAe;MAChB;eAZH;MAaC;MAEC,oBAAC,MAAD,EAAM,CAAA;MACN,oBAAC,QAAD;OAAM,OAAO,EAAE,UAAU,QAAQ;iBAAE;OAAa,CAAA;MAC5C;OACF;;GACF,CAAA;;AAKV,QACE,oBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,QAAQ,GAAG,cAAc;GACzB,MAAM;GACN,WAAW;GACX,SAAS,WAAW,KAAM;GAC1B,eAAe,WAAW,SAAS;GACnC,YAAY;GACZ,QAAQ;GACT;EACD,eAAY;YAEZ,qBAAC,UAAD;GACE,cAAc;GACd,aAAa;GACb,WAAW;GACX,eAAe,iBAAiB,cAAc;GAC9C,cAAc,iBAAiB,KAAK;GACpC,OAAO;IACL,OAAO;IACP,QAAQ;IACR,cAAc;IACd,YAAY,QAAQ,mBAAmB,EAAE,IAAI,mBAAmB,EAAE,IAAI,mBAAmB,EAAE;IAC3F,QAAQ,kBAAkB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE;IACtE,UAAU;IACV,OAAO;IACP,YAAY;IACZ,SAAS;IACT,eAAe;IACf,YAAY;IACZ,gBAAgB;IAChB,QAAQ;IACR,YAAY;IACZ,aAAa;IACb,YAAY;IACZ,WAAW,gDAAgD,mBAAmB,EAAE,IAAI,mBAAmB,EAAE,IAAI,mBAAmB,EAAE;IAClI,GAAG,cAAc,kBAAkB,eAAe;KAChD,WAAW,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,uDAAuD,mBAAmB,EAAE,IAAI,mBAAmB,EAAE,IAAI,mBAAmB,EAAE;KAChN,cAAc,cAAc;KAC5B,cAAc;KACf,CAAC;IACH;GACS;GACV,cACE,qBACE,UAAU,oBAAoB,eAAe,GAAG,gBAAgB,eAAe,YAC/E,mBAAmB,sBAAsB,eAAe,qBACzD,CAAC;GAEJ,iBAAe;GACf,iBAAc;GACd,MAAK;GACL,UAAU,WAAW,KAAK;GAC1B,eAAY;aAzCd,CA2CE,oBAAC,OAAD;IAAK,OAAO;KAAE,UAAU;KAAQ,YAAY;KAAG;cAC5C,gBAAgB;IACb,CAAA,EACN,oBAAC,OAAD;IAAK,OAAO;KAAE,UAAU;KAAQ,WAAW;KAAO;cAC/C,oBAAoB;IACjB,CAAA,CACC;;EACL,CAAA"}
1
+ {"version":3,"file":"StanceWheelPure.js","names":[],"sources":["../../../../src/components/shared/mobile/StanceWheelPure.tsx"],"sourcesContent":["/**\n * StanceWheelPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Circular 8-segment stance selector for mobile touch controls\n * Provides visual and tactile stance switching interface\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 stance buttons\n * - Keyboard navigation (Tab, Enter, Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - aria-expanded state for wheel toggle\n * - role=\"radiogroup\" for stance selection\n * - 50x50px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/StanceWheelPure\n * @category Mobile Controls\n * @korean 자세 휠 (순수 DOM)\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../types/common\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport {\n handleKeyboardNav,\n getFocusStyle,\n announceToScreenReader,\n} from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\n\n/**\n * Props for StanceWheelPure component\n */\nexport interface StanceWheelPureProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Callback when stance changes */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Whether wheel is expanded */\n readonly expanded: boolean;\n /** Callback to toggle expansion */\n readonly onToggle: () => void;\n /** Whether wheel is disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 when collapsed, 100 when expanded for safe area) */\n readonly bottom?: number;\n /** Opacity of wheel (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Trigram symbols for each stance\n */\nconst TRIGRAM_SYMBOLS = [\n \"☰\",\n \"☱\",\n \"☲\",\n \"☳\",\n \"☴\",\n \"☵\",\n \"☶\",\n \"☷\",\n] as const;\n\n/**\n * Korean names for each stance\n */\nconst STANCE_KOREAN_NAMES = [\n \"건\", // Geon - Heaven\n \"태\", // Tae - Lake\n \"리\", // Li - Fire\n \"진\", // Jin - Thunder\n \"손\", // Son - Wind\n \"감\", // Gam - Water\n \"간\", // Gan - Mountain\n \"곤\", // Gon - Earth\n] as const;\n\n/**\n * Get color for a specific stance\n */\nconst getStanceColor = (stance: TrigramStance): number => {\n const stanceColors: Record<TrigramStance, number> = {\n geon: 0xffd700, // Gold - Heaven\n tae: 0x00ffff, // Cyan - Lake\n li: 0xff4444, // Red - Fire\n jin: 0xffaa00, // Orange - Thunder\n son: 0x88ff88, // Light Green - Wind\n gam: 0x0088ff, // Blue - Water\n gan: 0x8844ff, // Purple - Mountain\n gon: 0xaa6644, // Brown - Earth\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * StanceWheelPure Component\n *\n * Pure DOM circular stance selector with 8 segments for trigram stances\n * Features:\n * - Expandable/collapsible interface\n * - Visual stance indicator when collapsed (60x60px)\n * - 8 touch-optimized stance buttons when expanded (50x50px each)\n * - 200px wheel diameter with safe positioning\n * - Korean trigram symbols and names\n * - Color-coded by stance element\n * - Haptic feedback on selection\n * - 50x50px minimum touch targets\n *\n * Usage in Combat:\n * - Tap collapsed indicator to expand wheel\n * - Select from 8 trigram stances\n * - Current stance highlighted with gold accent\n * - Tap current stance to collapse wheel\n *\n * @example\n * ```tsx\n * <StanceWheelPure\n * currentStance={player.stance}\n * onStanceChange={(index) => handleStanceChange(index)}\n * expanded={wheelExpanded}\n * onToggle={() => setWheelExpanded(!wheelExpanded)}\n * disabled={isPaused}\n * />\n * ```\n *\n * @public\n * @korean 자세휠순수\n */\nexport const StanceWheelPure: React.FC<StanceWheelPureProps> = ({\n currentStance,\n onStanceChange,\n expanded,\n onToggle,\n disabled = false,\n bottom,\n opacity = 0.8,\n}) => {\n const [hoveredStance, setHoveredStance] = useState<number | null>(null);\n const [focusedStance, setFocusedStance] = useState<number | null>(null);\n\n /**\n * Handle stance selection (touch or mouse)\n */\n const handleStanceSelect = useCallback(\n (e: React.TouchEvent | React.MouseEvent, stanceIndex: number) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n // Don't allow selecting the same stance\n if (stanceIndex === currentStance) {\n // Collapse wheel if tapping current stance\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\n // Announce stance change to screen readers\n const stanceName = STANCE_KOREAN_NAMES[stanceIndex];\n const stanceSymbol = TRIGRAM_SYMBOLS[stanceIndex];\n announceToScreenReader({\n message: createBilingualLabel(\n `자세 변경: ${stanceName} ${stanceSymbol}`,\n `Stance changed to ${TRIGRAM_STANCES_ORDER[stanceIndex]}`,\n ).label,\n politeness: \"polite\",\n });\n\n // Auto-collapse after selection (optional)\n // onToggle();\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle wheel toggle (touch or mouse)\n */\n const handleToggle = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n onToggle();\n triggerHaptic(\"light\");\n\n // Announce state change to screen readers using the toggled value\n const nextExpanded = !expanded;\n announceToScreenReader({\n message: createBilingualLabel(\n nextExpanded ? \"자세 휠 열림\" : \"자세 휠 닫힘\",\n nextExpanded ? \"Stance wheel opened\" : \"Stance wheel closed\",\n ).label,\n politeness: \"polite\",\n });\n },\n [disabled, onToggle, expanded],\n );\n\n /**\n * Handle keyboard navigation for stance buttons\n */\n const handleStanceKeyDown = useCallback(\n (stanceIndex: number) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n if (stanceIndex === currentStance) {\n onToggle();\n } else {\n onStanceChange(stanceIndex);\n }\n triggerHaptic(\"medium\");\n },\n onCancel: () => {\n onToggle();\n },\n onNavigate: (direction) => {\n // Navigate between stances with arrow keys\n let newIndex = stanceIndex;\n if (direction === \"left\" || direction === \"up\") {\n newIndex = (stanceIndex - 1 + 8) % 8;\n } else if (direction === \"right\" || direction === \"down\") {\n newIndex = (stanceIndex + 1) % 8;\n }\n setFocusedStance(newIndex);\n // Focus the new stance button on the next animation frame\n requestAnimationFrame(() => {\n const button = document.querySelector(\n `[data-testid=\"stance-button-pure-${newIndex}\"]`,\n ) as HTMLElement | null;\n button?.focus();\n });\n },\n });\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle keyboard navigation for toggle button\n */\n const handleToggleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n onToggle();\n triggerHaptic(\"light\");\n },\n });\n },\n [disabled, onToggle],\n );\n\n // Dynamic bottom position with safe area consideration\n const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\n // Get RGB values for colors using shared utility\n const currentStanceColor = getColorRGB(\n getStanceColor(TRIGRAM_STANCES_ORDER[currentStance]),\n );\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n if (expanded) {\n // Expanded: Show full 8-segment wheel\n const wheelSize = 200;\n const segmentAngle = 360 / 8;\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-expanded\"\n role=\"radiogroup\"\n aria-label={\n createBilingualLabel(\n \"팔괘 자세 선택\",\n \"Eight Trigram Stance Selection\",\n ).label\n }\n aria-activedescendant={`stance-button-pure-${currentStance}`}\n >\n <div\n style={{\n width: `${wheelSize}px`,\n height: `${wheelSize}px`,\n position: \"relative\",\n }}\n >\n {/* Stance buttons arranged in circle */}\n {TRIGRAM_STANCES_ORDER.map((stance, index) => {\n const angle = index * segmentAngle;\n const radian = (angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * 80 + 100;\n const y = Math.sin(radian) * 80 + 100;\n const isActive = index === currentStance;\n const isHovered = index === hoveredStance;\n const stanceColor = getColorRGB(getStanceColor(stance));\n\n return (\n <button\n key={stance}\n onTouchStart={(e) => handleStanceSelect(e, index)}\n onTouchEnd={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setHoveredStance(null);\n }}\n onMouseDown={(e) => handleStanceSelect(e, index)}\n onMouseEnter={() => setHoveredStance(index)}\n onMouseLeave={() => setHoveredStance(null)}\n onKeyDown={handleStanceKeyDown(index)}\n onFocus={() => setFocusedStance(index)}\n onBlur={() => setFocusedStance(null)}\n style={{\n position: \"absolute\",\n left: `${x - 25}px`,\n top: `${y - 25}px`,\n width: \"50px\",\n height: \"50px\",\n borderRadius: \"50%\",\n background: isActive\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.95)`\n : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`,\n border: `3px solid ${isActive ? \"#fff\" : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 1)`}`,\n fontSize: \"24px\",\n color: isActive ? \"#000\" : \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n transform: isActive || isHovered ? \"scale(1.15)\" : \"scale(1)\",\n boxShadow: isActive\n ? `0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : isHovered\n ? `0 0 15px rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`\n : `0 4px 10px rgba(0, 0, 0, 0.5)`,\n ...getFocusStyle(focusedStance === index, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n aria-label={\n createBilingualLabel(\n `${STANCE_KOREAN_NAMES[index]} ${TRIGRAM_SYMBOLS[index]}`,\n `${stance} stance`,\n ).label\n }\n aria-checked={isActive}\n role=\"radio\"\n tabIndex={0}\n disabled={disabled}\n id={`stance-button-pure-${index}`}\n data-testid={`stance-button-pure-${index}`}\n >\n <div style={{ fontSize: \"20px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[index]}\n </div>\n <div style={{ fontSize: \"8px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[index]}\n </div>\n </button>\n );\n })}\n\n {/* Center label */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontSize: \"12px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n textAlign: \"center\",\n pointerEvents: \"none\",\n }}\n >\n 자세 선택\n <br />\n <span style={{ fontSize: \"10px\" }}>Stance</span>\n </div>\n </div>\n </div>\n );\n }\n\n // Collapsed: Show current stance indicator\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-collapsed\"\n >\n <button\n onTouchStart={handleToggle}\n onMouseDown={handleToggle}\n onKeyDown={handleToggleKeyDown}\n onFocus={() => setFocusedStance(currentStance)}\n onBlur={() => setFocusedStance(null)}\n style={{\n width: \"60px\",\n height: \"60px\",\n borderRadius: \"50%\",\n background: `rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.9)`,\n border: `3px solid rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n fontSize: \"28px\",\n color: \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n ...getFocusStyle(focusedStance === currentStance, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n disabled={disabled}\n aria-label={\n createBilingualLabel(\n `현재 자세: ${STANCE_KOREAN_NAMES[currentStance]} ${TRIGRAM_SYMBOLS[currentStance]}. 자세 휠 열기`,\n `Current stance: ${TRIGRAM_STANCES_ORDER[currentStance]}. Open stance wheel`,\n ).label\n }\n aria-expanded={expanded}\n aria-haspopup=\"menu\"\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"stance-wheel-pure-toggle\"\n >\n <div style={{ fontSize: \"24px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[currentStance]}\n </div>\n <div style={{ fontSize: \"10px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[currentStance]}\n </div>\n </button>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TouchOptimizer.js","names":[],"sources":["../../../../src/components/shared/mobile/TouchOptimizer.ts"],"sourcesContent":["/**\n * TouchOptimizer\n * \n * High-performance touch event handling with <16ms latency\n * Uses requestAnimationFrame for immediate visual updates and\n * requestIdleCallback for deferred state updates to maintain 60fps\n * \n * Key Features:\n * - RAF-based visual updates (<16ms latency)\n * - Touch event coalescing (60-70% overhead reduction)\n * - Passive event listeners where appropriate\n * - Transform-only CSS animations (GPU-accelerated)\n * \n * @module components/mobile/TouchOptimizer\n * @category Mobile Controls\n * @korean 터치 최적화\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Touch position data\n */\nexport interface TouchPosition {\n readonly x: number;\n readonly y: number;\n readonly timestamp: number;\n}\n\n/**\n * Touch optimization options\n */\nexport interface TouchOptimizerOptions {\n /** Enable touch event coalescing (default: true) */\n readonly enableCoalescing?: boolean;\n /** Use passive listeners where possible (default: true) */\n readonly usePassiveListeners?: boolean;\n /** Enable RAF for visual updates (default: true) */\n readonly useRAF?: boolean;\n /** Coalescing sample rate (default: 3 - use last 3 events) */\n readonly coalescingSampleRate?: number;\n}\n\n/**\n * Touch optimizer return type\n */\nexport interface TouchOptimizerReturn {\n /** Current RAF ID (for debugging) */\n readonly rafId: number | null;\n /** Whether touch is active */\n readonly isTouching: boolean;\n}\n\n/**\n * Custom hook for optimized touch handling with <16ms latency\n * \n * Uses requestAnimationFrame for immediate visual feedback and\n * defers state updates to avoid blocking the main thread\n * \n * @param onTouchStart - Callback for touch start (immediate)\n * @param onTouchMove - Callback for touch move (coalesced)\n * @param onTouchEnd - Callback for touch end (immediate)\n * @param options - Optimization options\n * \n * @example\n * ```tsx\n * const { isTouching } = useTouchOptimizer(\n * (x, y) => {\n * // Immediate visual update (same frame)\n * buttonRef.current.style.transform = 'scale(0.95)';\n * \n * // Defer state update\n * requestIdleCallback(() => {\n * setPressed(true);\n * onAction();\n * });\n * },\n * (x, y) => {\n * // Handle coalesced touch move\n * updatePosition(x, y);\n * },\n * () => {\n * // Immediate visual reset\n * buttonRef.current.style.transform = 'scale(1)';\n * \n * requestIdleCallback(() => {\n * setPressed(false);\n * });\n * }\n * );\n * ```\n * \n * @public\n * @korean 터치최적화사용\n */\nexport function useTouchOptimizer(\n onTouchStart: (x: number, y: number, timestamp: number) => void,\n onTouchMove: (x: number, y: number, timestamp: number) => void,\n onTouchEnd: (x: number, y: number, timestamp: number) => void,\n options: TouchOptimizerOptions = {}\n): TouchOptimizerReturn {\n const {\n enableCoalescing = true,\n usePassiveListeners = true,\n useRAF = true,\n coalescingSampleRate = 3,\n } = options;\n\n const rafIdRef = useRef<number | null>(null);\n const touchStateRef = useRef<TouchPosition | null>(null);\n const isTouchingRef = useRef<boolean>(false);\n const pendingMoveRef = useRef<TouchPosition | null>(null);\n \n // State for returning values (not using refs in return)\n const [rafId, setRafId] = useState<number | null>(null);\n const [isTouching, setIsTouching] = useState<boolean>(false);\n\n /**\n * Cancel pending RAF\n */\n const cancelPendingRAF = useCallback(() => {\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n rafIdRef.current = null;\n setRafId(null);\n }\n }, []);\n\n /**\n * Process touch start with RAF\n */\n const processTouchStart = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n touchStateRef.current = { x, y, timestamp };\n isTouchingRef.current = true;\n setIsTouching(true);\n\n if (useRAF) {\n // Schedule immediate visual update (next frame, <16ms)\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchStart(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n // Direct call (no RAF)\n onTouchStart(x, y, timestamp);\n }\n },\n [onTouchStart, useRAF, cancelPendingRAF]\n );\n\n /**\n * Process coalesced touch move with RAF\n */\n const processTouchMove = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n pendingMoveRef.current = { x, y, timestamp };\n\n if (useRAF && rafIdRef.current === null) {\n // Schedule update for next frame\n rafIdRef.current = requestAnimationFrame(() => {\n if (pendingMoveRef.current) {\n const { x, y, timestamp } = pendingMoveRef.current;\n onTouchMove(x, y, timestamp);\n pendingMoveRef.current = null;\n }\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else if (!useRAF) {\n // Direct call (no RAF)\n onTouchMove(x, y, timestamp);\n }\n },\n [onTouchMove, useRAF]\n );\n\n /**\n * Process touch end with RAF\n */\n const processTouchEnd = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n\n if (useRAF) {\n // Schedule immediate visual update (next frame, <16ms)\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchEnd(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n // Direct call (no RAF)\n onTouchEnd(x, y, timestamp);\n }\n },\n [onTouchEnd, useRAF, cancelPendingRAF]\n );\n\n /**\n * Handle touch start event\n */\n const handleTouchStart = useCallback(\n (e: TouchEvent) => {\n // Prevent default to eliminate 300ms delay\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.touches[0];\n if (touch) {\n processTouchStart(touch.clientX, touch.clientY);\n }\n },\n [processTouchStart, usePassiveListeners]\n );\n\n /**\n * Handle touch move event with coalescing\n */\n const handleTouchMove = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n // Get coalesced events for smooth tracking\n let events: readonly Touch[] = [e.touches[0]];\n \n if (enableCoalescing) {\n // Feature detection: getCoalescedEvents() is experimental (Chrome 58+, Edge 79+)\n // Not supported in Safari or Firefox as of 2024\n // Fallback to single event if not supported\n const eventWithCoalescing = e as TouchEvent & { getCoalescedEvents?: () => TouchEvent[] };\n if (typeof eventWithCoalescing.getCoalescedEvents === 'function') {\n try {\n const coalesced = eventWithCoalescing.getCoalescedEvents();\n if (coalesced && coalesced.length > 0) {\n // Use only the last N events to reduce overhead\n const recentEvents = coalesced.slice(-coalescingSampleRate);\n events = recentEvents.map((evt: TouchEvent) => evt.touches[0]).filter((touch): touch is Touch => touch !== undefined);\n }\n } catch {\n // Fallback to single event if getCoalescedEvents fails (expected in Safari/Firefox)\n }\n }\n }\n\n // Process only the last event (most recent position)\n const lastTouch = events[events.length - 1];\n if (lastTouch) {\n processTouchMove(lastTouch.clientX, lastTouch.clientY);\n }\n },\n [enableCoalescing, coalescingSampleRate, processTouchMove]\n );\n\n /**\n * Handle touch end event\n */\n const handleTouchEnd = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n // Prevent default to eliminate delays\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.changedTouches[0];\n if (touch) {\n processTouchEnd(touch.clientX, touch.clientY);\n }\n },\n [processTouchEnd, usePassiveListeners]\n );\n\n /**\n * Handle touch cancel event\n */\n const handleTouchCancel = useCallback(() => {\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n cancelPendingRAF();\n }, [cancelPendingRAF]);\n\n /**\n * Setup touch event listeners\n * \n * Note: Event listeners are attached to the document for each component instance.\n * This allows independent touch handling per component but may result in multiple\n * document-level listeners if many components use this hook simultaneously.\n * For applications with many touch-optimized components, consider implementing\n * an event delegation pattern or singleton event manager for better efficiency.\n */\n useEffect(() => {\n const options: AddEventListenerOptions = {\n passive: usePassiveListeners,\n };\n\n document.addEventListener('touchstart', handleTouchStart, options);\n document.addEventListener('touchmove', handleTouchMove, options);\n document.addEventListener('touchend', handleTouchEnd, options);\n document.addEventListener('touchcancel', handleTouchCancel, options);\n\n return () => {\n document.removeEventListener('touchstart', handleTouchStart);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n document.removeEventListener('touchcancel', handleTouchCancel);\n cancelPendingRAF();\n };\n }, [\n handleTouchStart,\n handleTouchMove,\n handleTouchEnd,\n handleTouchCancel,\n usePassiveListeners,\n cancelPendingRAF,\n ]);\n\n return {\n rafId,\n isTouching,\n };\n}\n\n/**\n * Helper function to create optimized visual updates\n * Updates DOM directly for immediate feedback, defers state\n * \n * @param element - DOM element to update\n * @param visualUpdate - Function to update visual state (runs in RAF)\n * @param stateUpdate - Function to update React state (runs in idle)\n * \n * @example\n * ```tsx\n * applyOptimizedUpdate(\n * buttonRef.current,\n * (el) => {\n * // Immediate visual feedback (<16ms)\n * el.style.transform = 'scale(0.95)';\n * el.style.filter = 'brightness(1.2)';\n * },\n * () => {\n * // Deferred state update (non-blocking)\n * setPressed(true);\n * onAction();\n * }\n * );\n * ```\n * \n * @public\n * @korean 최적화된업데이트적용\n */\nexport function applyOptimizedUpdate(\n element: HTMLElement | null,\n visualUpdate: (element: HTMLElement) => void,\n stateUpdate: () => void\n): void {\n // Immediate visual update (same frame)\n if (element) {\n requestAnimationFrame(() => {\n visualUpdate(element);\n });\n }\n\n // Deferred state update (when idle)\n if (typeof (window as Window & typeof globalThis).requestIdleCallback === 'function') {\n (window as Window & typeof globalThis).requestIdleCallback(() => {\n stateUpdate();\n });\n } else {\n // Fallback for browsers without requestIdleCallback\n setTimeout(stateUpdate, 0);\n }\n}\n\n/**\n * Create transform-only style for GPU-accelerated animations\n * Avoids layout thrashing by only using transform\n * \n * @param pressed - Whether element is pressed\n * @param scale - Scale value when pressed (default: 0.95)\n * \n * @returns CSS transform string\n * \n * @example\n * ```tsx\n * const style = {\n * transform: createTransformStyle(isPressed, 0.95),\n * transition: 'transform 0.1s ease-out',\n * willChange: 'transform', // Hint to GPU\n * };\n * ```\n * \n * @public\n * @korean 변환스타일생성\n */\nexport function createTransformStyle(\n pressed: boolean,\n scale: number = 0.95\n): string {\n if (pressed) {\n return `scale(${scale})`;\n }\n return 'scale(1)';\n}\n\n/**\n * Create filter style for visual effects\n * \n * @param pressed - Whether element is pressed\n * @param brightness - Brightness multiplier when pressed (default: 1.2)\n * \n * @returns CSS filter string\n * \n * @public\n * @korean 필터스타일생성\n */\nexport function createFilterStyle(\n pressed: boolean,\n brightness: number = 1.2\n): string {\n if (pressed) {\n return `brightness(${brightness})`;\n }\n return 'brightness(1)';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+WA,SAAgB,qBACd,SACA,cACA,aACM;AAEN,KAAI,QACF,6BAA4B;AAC1B,eAAa,QAAQ;GACrB;AAIJ,KAAI,OAAQ,OAAsC,wBAAwB,WACvE,QAAsC,0BAA0B;AAC/D,eAAa;GACb;KAGF,YAAW,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;AAyB9B,SAAgB,qBACd,SACA,QAAgB,KACR;AACR,KAAI,QACF,QAAO,SAAS,MAAM;AAExB,QAAO;;;;;;;;;;;;;AAcT,SAAgB,kBACd,SACA,aAAqB,KACb;AACR,KAAI,QACF,QAAO,cAAc,WAAW;AAElC,QAAO"}
1
+ {"version":3,"file":"TouchOptimizer.js","names":[],"sources":["../../../../src/components/shared/mobile/TouchOptimizer.ts"],"sourcesContent":["/**\n * TouchOptimizer\n * \n * High-performance touch event handling with <16ms latency\n * Uses requestAnimationFrame for immediate visual updates and\n * requestIdleCallback for deferred state updates to maintain 60fps\n * \n * Key Features:\n * - RAF-based visual updates (<16ms latency)\n * - Touch event coalescing (60-70% overhead reduction)\n * - Passive event listeners where appropriate\n * - Transform-only CSS animations (GPU-accelerated)\n * \n * @module components/mobile/TouchOptimizer\n * @category Mobile Controls\n * @korean 터치 최적화\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Touch position data\n */\nexport interface TouchPosition {\n readonly x: number;\n readonly y: number;\n readonly timestamp: number;\n}\n\n/**\n * Touch optimization options\n */\nexport interface TouchOptimizerOptions {\n /** Enable touch event coalescing (default: true) */\n readonly enableCoalescing?: boolean;\n /** Use passive listeners where possible (default: true) */\n readonly usePassiveListeners?: boolean;\n /** Enable RAF for visual updates (default: true) */\n readonly useRAF?: boolean;\n /** Coalescing sample rate (default: 3 - use last 3 events) */\n readonly coalescingSampleRate?: number;\n}\n\n/**\n * Touch optimizer return type\n */\nexport interface TouchOptimizerReturn {\n /** Current RAF ID (for debugging) */\n readonly rafId: number | null;\n /** Whether touch is active */\n readonly isTouching: boolean;\n}\n\n/**\n * Custom hook for optimized touch handling with <16ms latency\n * \n * Uses requestAnimationFrame for immediate visual feedback and\n * defers state updates to avoid blocking the main thread\n * \n * @param onTouchStart - Callback for touch start (immediate)\n * @param onTouchMove - Callback for touch move (coalesced)\n * @param onTouchEnd - Callback for touch end (immediate)\n * @param options - Optimization options\n * \n * @example\n * ```tsx\n * const { isTouching } = useTouchOptimizer(\n * (x, y) => {\n * // Immediate visual update (same frame)\n * buttonRef.current.style.transform = 'scale(0.95)';\n * \n * // Defer state update\n * requestIdleCallback(() => {\n * setPressed(true);\n * onAction();\n * });\n * },\n * (x, y) => {\n * // Handle coalesced touch move\n * updatePosition(x, y);\n * },\n * () => {\n * // Immediate visual reset\n * buttonRef.current.style.transform = 'scale(1)';\n * \n * requestIdleCallback(() => {\n * setPressed(false);\n * });\n * }\n * );\n * ```\n * \n * @public\n * @korean 터치최적화사용\n */\nexport function useTouchOptimizer(\n onTouchStart: (x: number, y: number, timestamp: number) => void,\n onTouchMove: (x: number, y: number, timestamp: number) => void,\n onTouchEnd: (x: number, y: number, timestamp: number) => void,\n options: TouchOptimizerOptions = {}\n): TouchOptimizerReturn {\n const {\n enableCoalescing = true,\n usePassiveListeners = true,\n useRAF = true,\n coalescingSampleRate = 3,\n } = options;\n\n const rafIdRef = useRef<number | null>(null);\n const touchStateRef = useRef<TouchPosition | null>(null);\n const isTouchingRef = useRef<boolean>(false);\n const pendingMoveRef = useRef<TouchPosition | null>(null);\n \n // State for returning values (not using refs in return)\n const [rafId, setRafId] = useState<number | null>(null);\n const [isTouching, setIsTouching] = useState<boolean>(false);\n\n /**\n * Cancel pending RAF\n */\n const cancelPendingRAF = useCallback(() => {\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n rafIdRef.current = null;\n setRafId(null);\n }\n }, []);\n\n /**\n * Process touch start with RAF\n */\n const processTouchStart = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n touchStateRef.current = { x, y, timestamp };\n isTouchingRef.current = true;\n setIsTouching(true);\n\n if (useRAF) {\n // Schedule immediate visual update (next frame, <16ms)\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchStart(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n // Direct call (no RAF)\n onTouchStart(x, y, timestamp);\n }\n },\n [onTouchStart, useRAF, cancelPendingRAF]\n );\n\n /**\n * Process coalesced touch move with RAF\n */\n const processTouchMove = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n pendingMoveRef.current = { x, y, timestamp };\n\n if (useRAF && rafIdRef.current === null) {\n // Schedule update for next frame\n rafIdRef.current = requestAnimationFrame(() => {\n if (pendingMoveRef.current) {\n const { x, y, timestamp } = pendingMoveRef.current;\n onTouchMove(x, y, timestamp);\n pendingMoveRef.current = null;\n }\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else if (!useRAF) {\n // Direct call (no RAF)\n onTouchMove(x, y, timestamp);\n }\n },\n [onTouchMove, useRAF]\n );\n\n /**\n * Process touch end with RAF\n */\n const processTouchEnd = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n\n if (useRAF) {\n // Schedule immediate visual update (next frame, <16ms)\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchEnd(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n // Direct call (no RAF)\n onTouchEnd(x, y, timestamp);\n }\n },\n [onTouchEnd, useRAF, cancelPendingRAF]\n );\n\n /**\n * Handle touch start event\n */\n const handleTouchStart = useCallback(\n (e: TouchEvent) => {\n // Prevent default to eliminate 300ms delay\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.touches[0];\n if (touch) {\n processTouchStart(touch.clientX, touch.clientY);\n }\n },\n [processTouchStart, usePassiveListeners]\n );\n\n /**\n * Handle touch move event with coalescing\n */\n const handleTouchMove = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n // Get coalesced events for smooth tracking\n let events: readonly Touch[] = [e.touches[0]];\n \n if (enableCoalescing) {\n // Feature detection: getCoalescedEvents() is experimental (Chrome 58+, Edge 79+)\n // Not supported in Safari or Firefox as of 2024\n // Fallback to single event if not supported\n const eventWithCoalescing = e as TouchEvent & { getCoalescedEvents?: () => TouchEvent[] };\n if (typeof eventWithCoalescing.getCoalescedEvents === 'function') {\n try {\n const coalesced = eventWithCoalescing.getCoalescedEvents();\n if (coalesced && coalesced.length > 0) {\n // Use only the last N events to reduce overhead\n const recentEvents = coalesced.slice(-coalescingSampleRate);\n events = recentEvents.map((evt: TouchEvent) => evt.touches[0]).filter((touch): touch is Touch => touch !== undefined);\n }\n } catch {\n // Fallback to single event if getCoalescedEvents fails (expected in Safari/Firefox)\n }\n }\n }\n\n // Process only the last event (most recent position)\n const lastTouch = events[events.length - 1];\n if (lastTouch) {\n processTouchMove(lastTouch.clientX, lastTouch.clientY);\n }\n },\n [enableCoalescing, coalescingSampleRate, processTouchMove]\n );\n\n /**\n * Handle touch end event\n */\n const handleTouchEnd = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n // Prevent default to eliminate delays\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.changedTouches[0];\n if (touch) {\n processTouchEnd(touch.clientX, touch.clientY);\n }\n },\n [processTouchEnd, usePassiveListeners]\n );\n\n /**\n * Handle touch cancel event\n */\n const handleTouchCancel = useCallback(() => {\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n cancelPendingRAF();\n }, [cancelPendingRAF]);\n\n /**\n * Setup touch event listeners\n * \n * Note: Event listeners are attached to the document for each component instance.\n * This allows independent touch handling per component but may result in multiple\n * document-level listeners if many components use this hook simultaneously.\n * For applications with many touch-optimized components, consider implementing\n * an event delegation pattern or singleton event manager for better efficiency.\n */\n useEffect(() => {\n const options: AddEventListenerOptions = {\n passive: usePassiveListeners,\n };\n\n document.addEventListener('touchstart', handleTouchStart, options);\n document.addEventListener('touchmove', handleTouchMove, options);\n document.addEventListener('touchend', handleTouchEnd, options);\n document.addEventListener('touchcancel', handleTouchCancel, options);\n\n return () => {\n document.removeEventListener('touchstart', handleTouchStart);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n document.removeEventListener('touchcancel', handleTouchCancel);\n cancelPendingRAF();\n };\n }, [\n handleTouchStart,\n handleTouchMove,\n handleTouchEnd,\n handleTouchCancel,\n usePassiveListeners,\n cancelPendingRAF,\n ]);\n\n return {\n rafId,\n isTouching,\n };\n}\n\n/**\n * Helper function to create optimized visual updates\n * Updates DOM directly for immediate feedback, defers state\n * \n * @param element - DOM element to update\n * @param visualUpdate - Function to update visual state (runs in RAF)\n * @param stateUpdate - Function to update React state (runs in idle)\n * \n * @example\n * ```tsx\n * applyOptimizedUpdate(\n * buttonRef.current,\n * (el) => {\n * // Immediate visual feedback (<16ms)\n * el.style.transform = 'scale(0.95)';\n * el.style.filter = 'brightness(1.2)';\n * },\n * () => {\n * // Deferred state update (non-blocking)\n * setPressed(true);\n * onAction();\n * }\n * );\n * ```\n * \n * @public\n * @korean 최적화된업데이트적용\n */\nexport function applyOptimizedUpdate(\n element: HTMLElement | null,\n visualUpdate: (element: HTMLElement) => void,\n stateUpdate: () => void\n): void {\n // Immediate visual update (same frame)\n if (element) {\n requestAnimationFrame(() => {\n visualUpdate(element);\n });\n }\n\n // Deferred state update (when idle)\n if (typeof (window as Window & typeof globalThis).requestIdleCallback === 'function') {\n (window as Window & typeof globalThis).requestIdleCallback(() => {\n stateUpdate();\n });\n } else {\n // Fallback for browsers without requestIdleCallback\n setTimeout(stateUpdate, 0);\n }\n}\n\n/**\n * Create transform-only style for GPU-accelerated animations\n * Avoids layout thrashing by only using transform\n * \n * @param pressed - Whether element is pressed\n * @param scale - Scale value when pressed (default: 0.95)\n * \n * @returns CSS transform string\n * \n * @example\n * ```tsx\n * const style = {\n * transform: createTransformStyle(isPressed, 0.95),\n * transition: 'transform 0.1s ease-out',\n * willChange: 'transform', // Hint to GPU\n * };\n * ```\n * \n * @public\n * @korean 변환스타일생성\n */\nexport function createTransformStyle(\n pressed: boolean,\n scale: number = 0.95\n): string {\n if (pressed) {\n return `scale(${scale})`;\n }\n return 'scale(1)';\n}\n\n/**\n * Create filter style for visual effects\n * \n * @param pressed - Whether element is pressed\n * @param brightness - Brightness multiplier when pressed (default: 1.2)\n * \n * @returns CSS filter string\n * \n * @public\n * @korean 필터스타일생성\n */\nexport function createFilterStyle(\n pressed: boolean,\n brightness: number = 1.2\n): string {\n if (pressed) {\n return `brightness(${brightness})`;\n }\n return 'brightness(1)';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+WA,SAAgB,qBACd,SACA,cACA,aACM;CAEN,IAAI,SACF,4BAA4B;EAC1B,aAAa,QAAQ;GACrB;CAIJ,IAAI,OAAQ,OAAsC,wBAAwB,YACxE,OAAuC,0BAA0B;EAC/D,aAAa;GACb;MAGF,WAAW,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;;AAyB9B,SAAgB,qBACd,SACA,QAAgB,KACR;CACR,IAAI,SACF,OAAO,SAAS,MAAM;CAExB,OAAO;;;;;;;;;;;;;AAcT,SAAgB,kBACd,SACA,aAAqB,KACb;CACR,IAAI,SACF,OAAO,cAAc,WAAW;CAElC,OAAO"}