blacktrigram 0.7.98 → 0.7.99
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.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioManager.js +2 -4
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +8 -9
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +2 -4
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +10 -9
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js +19 -18
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js +2 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js +0 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +12 -3
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/systems/CombatSystem.js +6 -2
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js +0 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js +10 -21
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js +3 -4
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js +3 -9
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/types/facial.js.map +1 -1
- package/lib/utils/accessibility.js +0 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/effectUtils.js +1 -3
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/inputSystem.js +0 -2
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js +6 -3
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/playerUtils.js +4 -2
- package/lib/utils/playerUtils.js.map +1 -1
- package/package.json +5 -5
|
@@ -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\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 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 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 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 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 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 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\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 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 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 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 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 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 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 * @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 if (stanceIndex === currentStance) {\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\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 },\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 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 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 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 const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\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 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 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
|
+
{"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 * @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 if (stanceIndex === currentStance) {\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\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 },\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 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 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 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 const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\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 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 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":"VirtualDPad.js","names":[],"sources":["../../../../src/components/shared/mobile/VirtualDPad.tsx"],"sourcesContent":["/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for mobile touch controls\n * Provides tactile movement control with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 directions\n * - Keyboard navigation (Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - role=\"group\" with descriptive label\n * - 48x48px minimum touch targets\n *\n * @module components/mobile/VirtualDPad\n * @category Mobile Controls\n * @korean 가상 방향 패드\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useState, useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * 8 directions for movement control\n */\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\n/**\n * Event type for D-Pad interactions\n */\nexport type DPadEventType = \"start\" | \"end\";\n\n/**\n * Props for VirtualDPad component\n */\nexport interface VirtualDPadProps {\n /** Callback when direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Whether D-Pad is disabled */\n readonly disabled?: boolean;\n /** Size of the D-Pad in pixels (default: 140) */\n readonly size?: number;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from left in pixels (default: 20) */\n readonly left?: number;\n /** Opacity of the D-Pad (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number; // Angle in degrees for positioning\n readonly korean: string; // Korean label\n readonly englishLabel: string; // English label for ARIA\n}\n\n/**\n * 8-directional configuration\n * Arranged clockwise starting from up (0°)\n */\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, korean: \"↑\", englishLabel: \"Up\" },\n { direction: \"up-right\", angle: 45, korean: \"↗\", englishLabel: \"Up Right\" },\n { direction: \"right\", angle: 90, korean: \"→\", englishLabel: \"Right\" },\n {\n direction: \"down-right\",\n angle: 135,\n korean: \"↘\",\n englishLabel: \"Down Right\",\n },\n { direction: \"down\", angle: 180, korean: \"↓\", englishLabel: \"Down\" },\n {\n direction: \"down-left\",\n angle: 225,\n korean: \"↙\",\n englishLabel: \"Down Left\",\n },\n { direction: \"left\", angle: 270, korean: \"←\", englishLabel: \"Left\" },\n { direction: \"up-left\", angle: 315, korean: \"↖\", englishLabel: \"Up Left\" },\n] as const;\n\n/**\n * Individual D-Pad button component\n */\ninterface DPadButtonProps {\n readonly config: DirectionConfig;\n readonly active: boolean;\n readonly focused: boolean;\n readonly onStart: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onEnd: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onKeyDown: (e: React.KeyboardEvent) => void;\n readonly onFocus: () => void;\n readonly onBlur: () => void;\n readonly radius: number; // Radius for button positioning\n readonly buttonSize: number;\n}\n\n/**\n * D-Pad button positioned around the center\n * Memoized to prevent unnecessary re-renders\n */\nconst DPadButton = React.memo<DPadButtonProps>(\n ({\n config,\n active,\n focused,\n onStart,\n onEnd,\n onKeyDown,\n onFocus,\n onBlur,\n radius,\n buttonSize,\n }) => {\n const radian = (config.angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * radius;\n const y = Math.sin(radian) * radius;\n\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n const ariaLabel = createBilingualLabel(\n `이동 ${config.korean}`,\n `Move ${config.englishLabel}`,\n ).label;\n\n return (\n <button\n onTouchStart={onStart}\n onTouchEnd={onEnd}\n onMouseDown={onStart}\n onMouseUp={onEnd}\n onMouseLeave={onEnd}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n onBlur={onBlur}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${buttonSize / 2}px)`,\n width: `${buttonSize}px`,\n height: `${buttonSize}px`,\n borderRadius: \"50%\",\n background: active\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : \"rgba(0, 0, 0, 0.5)\",\n border: focused\n ? `3px solid rgb(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b})`\n : `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${active ? 1 : 0.6})`,\n fontSize: \"20px\",\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.2s ease, opacity 0.2s ease\",\n transform: active ? \"scale(1.1)\" : \"scale(1)\",\n boxShadow: focused\n ? `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : active\n ? `0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : \"none\",\n ...getFocusStyle(focused),\n }}\n aria-label={ariaLabel}\n aria-pressed={active}\n role=\"button\"\n tabIndex={0}\n data-testid={`dpad-button-${config.direction}`}\n >\n {config.korean}\n </button>\n );\n },\n (prevProps, nextProps) => {\n return (\n prevProps.active === nextProps.active &&\n prevProps.focused === nextProps.focused\n );\n },\n);\n\nDPadButton.displayName = \"DPadButton\";\n\n/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for touch-based movement control\n * Features:\n * - 8 directional buttons arranged in a circle\n * - Visual feedback on active direction\n * - Haptic feedback on touch\n * - Korean theming with cyberpunk aesthetics\n * - 48x48px minimum touch targets (improved from iOS 44px guideline)\n * - 140x140px default size for better mobile usability\n *\n * Usage in Combat/Training:\n * - Provides tactical positioning and footwork\n * - Alternative to keyboard WASD controls\n * - Essential for mobile gameplay\n *\n * @example\n * ```tsx\n * <VirtualDPad\n * onMove={(direction, eventType) => {\n * if (eventType === 'start' && direction) {\n * handleMovement(direction);\n * } else if (eventType === 'end') {\n * stopMovement();\n * }\n * }}\n * disabled={isPaused}\n * size={140}\n * bottom={34}\n * />\n * ```\n *\n * @korean 가상방향패드\n */\nconst VirtualDPadComponent: React.FC<VirtualDPadProps> = ({\n onMove,\n disabled = false,\n size = 140,\n bottom = 34,\n left = 20,\n opacity = 0.8,\n}) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [focusedDirection, setFocusedDirection] = useState<Direction | null>(\n null,\n );\n\n const throttledOnMove = useThrottle(onMove, 16);\n\n /**\n * Handle touch or mouse start on a direction button\n * Throttled for performance\n */\n const handleStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle touch or mouse end\n * Throttled for performance\n */\n const handleEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle keyboard navigation for D-Pad\n */\n const handleKeyDown = useCallback(\n (direction: Direction) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n setTimeout(() => {\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n }, 150);\n },\n });\n },\n [disabled, throttledOnMove],\n );\n\n const dimensions = useMemo(\n () => ({\n buttonSize: Math.max(48, size * 0.3),\n radius: (size - Math.max(48, size * 0.3)) / 2,\n }),\n [size],\n );\n\n const colors = useMemo(\n () => ({\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n left: `${left}px`,\n width: `${size}px`,\n height: `${size}px`,\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"virtual-dpad\"\n >\n {/* D-Pad Container */}\n <div\n style={{\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n background: \"rgba(0, 0, 0, 0.5)\",\n borderRadius: \"50%\",\n border: `2px solid rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.8)`,\n boxShadow: `0 0 20px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.3)`,\n }}\n role=\"group\"\n aria-label={\n createBilingualLabel(\"방향 패드\", \"Directional Pad\").label\n }\n >\n {/* Directional Buttons */}\n {DIRECTIONS.map((config) => (\n <DPadButton\n key={config.direction}\n config={config}\n active={activeDirection === config.direction}\n focused={focusedDirection === config.direction}\n onStart={(e) => handleStart(e, config.direction)}\n onEnd={handleEnd}\n onKeyDown={handleKeyDown(config.direction)}\n onFocus={() => setFocusedDirection(config.direction)}\n onBlur={() => setFocusedDirection(null)}\n radius={dimensions.radius}\n buttonSize={dimensions.buttonSize}\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 ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`\n : `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.7)`,\n border: \"2px solid #fff\",\n transition: \"transform 0.15s ease, opacity 0.15s ease\",\n boxShadow: activeDirection\n ? `0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.8)`\n : \"none\",\n }}\n data-testid=\"dpad-center\"\n />\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized VirtualDPad with custom comparison\n * Only re-renders when props change\n */\nexport const VirtualDPad = React.memo(\n VirtualDPadComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.size === nextProps.size &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.left === nextProps.left &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onMove === nextProps.onMove\n );\n },\n);\n\nVirtualDPad.displayName = \"VirtualDPad\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,IAAM,aAAyC;CAC7C;EAAE,WAAW;EAAM,OAAO;EAAG,QAAQ;EAAK,cAAc;CAAK;CAC7D;EAAE,WAAW;EAAY,OAAO;EAAI,QAAQ;EAAK,cAAc;CAAW;CAC1E;EAAE,WAAW;EAAS,OAAO;EAAI,QAAQ;EAAK,cAAc;CAAQ;CACpE;EACE,WAAW;EACX,OAAO;EACP,QAAQ;EACR,cAAc;CAChB;CACA;EAAE,WAAW;EAAQ,OAAO;EAAK,QAAQ;EAAK,cAAc;CAAO;CACnE;EACE,WAAW;EACX,OAAO;EACP,QAAQ;EACR,cAAc;CAChB;CACA;EAAE,WAAW;EAAQ,OAAO;EAAK,QAAQ;EAAK,cAAc;CAAO;CACnE;EAAE,WAAW;EAAW,OAAO;EAAK,QAAQ;EAAK,cAAc;CAAU;AAC3E;;;;;AAsBA,IAAM,aAAa,MAAM,MACtB,EACC,QACA,QACA,SACA,SACA,OACA,WACA,SACA,QACA,QACA,iBACI;CACJ,MAAM,UAAU,OAAO,QAAQ,OAAO,KAAK,KAAK;CAChD,MAAM,IAAI,KAAK,IAAI,MAAM,IAAI;CAC7B,MAAM,IAAI,KAAK,IAAI,MAAM,IAAI;CAE7B,MAAM,YAAY,YAAY,cAAc,WAAW;CACvD,MAAM,eAAe,YAAY,cAAc,YAAY;CAE3D,MAAM,YAAY,qBAChB,MAAM,OAAO,UACb,QAAQ,OAAO,cACjB,CAAC,CAAC;CAEF,OACE,oBAAC,UAAD;EACE,cAAc;EACd,YAAY;EACZ,aAAa;EACb,WAAW;EACX,cAAc;EACH;EACF;EACD;EACR,OAAO;GACL,UAAU;GACV,MAAM,cAAc,EAAE,OAAO,aAAa,EAAE;GAC5C,KAAK,cAAc,EAAE,OAAO,aAAa,EAAE;GAC3C,OAAO,GAAG,WAAW;GACrB,QAAQ,GAAG,WAAW;GACtB,cAAc;GACd,YAAY,SACR,QAAQ,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,UACpD;GACJ,QAAQ,UACJ,iBAAiB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,KACtE,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,SAAS,IAAI,GAAI;GAChG,UAAU;GACV,OAAO;GACP,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,QAAQ;GACR,YAAY;GACZ,aAAa;GACb,YAAY;GACZ,WAAW,SAAS,eAAe;GACnC,WAAW,UACP,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,wBAAwB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,UAC3I,SACE,iBAAiB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,UAC7D;GACN,GAAG,cAAc,OAAO;EAC1B;EACA,cAAY;EACZ,gBAAc;EACd,MAAK;EACL,UAAU;EACV,eAAa,eAAe,OAAO;YAElC,OAAO;CACF,CAAA;AAEZ,IACC,WAAW,cAAc;CACxB,OACE,UAAU,WAAW,UAAU,UAC/B,UAAU,YAAY,UAAU;AAEpC,CACF;AAEA,WAAW,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCzB,IAAM,wBAAoD,EACxD,QACA,WAAW,OACX,OAAO,KACP,SAAS,IACT,OAAO,IACP,UAAU,SACN;CACJ,MAAM,CAAC,iBAAiB,sBAAsB,SAC5C,IACF;CACA,MAAM,CAAC,kBAAkB,uBAAuB,SAC9C,IACF;CAEA,MAAM,kBAAkB,YAAY,QAAQ,EAAE;;;;;CAM9C,MAAM,cAAc,aACjB,GAAwC,cAAyB;EAChE,IAAI,UAAU;EACd,EAAE,eAAe;EACjB,EAAE,gBAAgB;EAElB,mBAAmB,SAAS;EAC5B,gBAAgB,WAAW,OAAO;EAClC,cAAc,OAAO;CACvB,GACA,CAAC,UAAU,eAAe,CAC5B;;;;;CAMA,MAAM,YAAY,aACf,MAA2C;EAC1C,IAAI,UAAU;EACd,EAAE,eAAe;EACjB,EAAE,gBAAgB;EAElB,mBAAmB,IAAI;EACvB,gBAAgB,MAAM,KAAK;CAC7B,GACA,CAAC,UAAU,eAAe,CAC5B;;;;CAKA,MAAM,gBAAgB,aACnB,eAA0B,MAA2B;EACpD,IAAI,UAAU;EACd,kBAAkB,EAAE,aAAa,EAC/B,kBAAkB;GAChB,mBAAmB,SAAS;GAC5B,gBAAgB,WAAW,OAAO;GAClC,cAAc,OAAO;GACrB,iBAAiB;IACf,mBAAmB,IAAI;IACvB,gBAAgB,MAAM,KAAK;GAC7B,GAAG,GAAG;EACR,EACF,CAAC;CACH,GACA,CAAC,UAAU,eAAe,CAC5B;CAEA,MAAM,aAAa,eACV;EACL,YAAY,KAAK,IAAI,IAAI,OAAO,EAAG;EACnC,SAAS,OAAO,KAAK,IAAI,IAAI,OAAO,EAAG,KAAK;CAC9C,IACA,CAAC,IAAI,CACP;CAEA,MAAM,SAAS,eACN;EACL,SAAS,YAAY,cAAc,YAAY;EAC/C,MAAM,YAAY,cAAc,WAAW;CAC7C,IACA,CAAC,CACH;CAEA,OACE,oBAAC,MAAD;EAAM,YAAA;YACJ,oBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,QAAQ,GAAG,OAAO;IAClB,MAAM,GAAG,KAAK;IACd,OAAO,GAAG,KAAK;IACf,QAAQ,GAAG,KAAK;IAChB,SAAS,WAAW,KAAM;IAC1B,eAAe,WAAW,SAAS;GACrC;GACA,eAAY;aAGZ,qBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,OAAO;KACP,QAAQ;KACR,YAAY;KACZ,cAAc;KACd,QAAQ,kBAAkB,OAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE;KACrF,WAAW,iBAAiB,OAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE;IACzF;IACA,MAAK;IACL,cACE,qBAAqB,SAAS,iBAAiB,CAAC,CAAC;cAZrD,CAgBG,WAAW,KAAK,WACf,oBAAC,YAAD;KAEU;KACR,QAAQ,oBAAoB,OAAO;KACnC,SAAS,qBAAqB,OAAO;KACrC,UAAU,MAAM,YAAY,GAAG,OAAO,SAAS;KAC/C,OAAO;KACP,WAAW,cAAc,OAAO,SAAS;KACzC,eAAe,oBAAoB,OAAO,SAAS;KACnD,cAAc,oBAAoB,IAAI;KACtC,QAAQ,WAAW;KACnB,YAAY,WAAW;IACxB,GAXM,OAAO,SAWb,CACF,GAGD,oBAAC,OAAD;KACE,OAAO;MACL,UAAU;MACV,KAAK;MACL,MAAM;MACN,WAAW;MACX,OAAO;MACP,QAAQ;MACR,cAAc;MACd,YAAY,kBACR,QAAQ,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,UAC1D,QAAQ,OAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE;MACvE,QAAQ;MACR,YAAY;MACZ,WAAW,kBACP,iBAAiB,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,UACnE;KACN;KACA,eAAY;IACb,CAAA,CACE;;EACF,CAAA;CACD,CAAA;AAEV;;;;;AAMA,IAAa,cAAc,MAAM,KAC/B,uBACC,WAAW,cAAc;CACxB,OACE,UAAU,aAAa,UAAU,YACjC,UAAU,SAAS,UAAU,QAC7B,UAAU,WAAW,UAAU,UAC/B,UAAU,SAAS,UAAU,QAC7B,UAAU,YAAY,UAAU,WAChC,UAAU,WAAW,UAAU;AAEnC,CACF;AAEA,YAAY,cAAc"}
|
|
1
|
+
{"version":3,"file":"VirtualDPad.js","names":[],"sources":["../../../../src/components/shared/mobile/VirtualDPad.tsx"],"sourcesContent":["/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for mobile touch controls\n * Provides tactile movement control with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 directions\n * - Keyboard navigation (Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - role=\"group\" with descriptive label\n * - 48x48px minimum touch targets\n *\n * @module components/mobile/VirtualDPad\n * @category Mobile Controls\n * @korean 가상 방향 패드\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useState, useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * 8 directions for movement control\n */\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\n/**\n * Event type for D-Pad interactions\n */\nexport type DPadEventType = \"start\" | \"end\";\n\n/**\n * Props for VirtualDPad component\n */\nexport interface VirtualDPadProps {\n /** Callback when direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Whether D-Pad is disabled */\n readonly disabled?: boolean;\n /** Size of the D-Pad in pixels (default: 140) */\n readonly size?: number;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from left in pixels (default: 20) */\n readonly left?: number;\n /** Opacity of the D-Pad (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number; // Angle in degrees for positioning\n readonly korean: string; // Korean label\n readonly englishLabel: string; // English label for ARIA\n}\n\n/**\n * 8-directional configuration\n * Arranged clockwise starting from up (0°)\n */\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, korean: \"↑\", englishLabel: \"Up\" },\n { direction: \"up-right\", angle: 45, korean: \"↗\", englishLabel: \"Up Right\" },\n { direction: \"right\", angle: 90, korean: \"→\", englishLabel: \"Right\" },\n {\n direction: \"down-right\",\n angle: 135,\n korean: \"↘\",\n englishLabel: \"Down Right\",\n },\n { direction: \"down\", angle: 180, korean: \"↓\", englishLabel: \"Down\" },\n {\n direction: \"down-left\",\n angle: 225,\n korean: \"↙\",\n englishLabel: \"Down Left\",\n },\n { direction: \"left\", angle: 270, korean: \"←\", englishLabel: \"Left\" },\n { direction: \"up-left\", angle: 315, korean: \"↖\", englishLabel: \"Up Left\" },\n] as const;\n\n/**\n * Individual D-Pad button component\n */\ninterface DPadButtonProps {\n readonly config: DirectionConfig;\n readonly active: boolean;\n readonly focused: boolean;\n readonly onStart: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onEnd: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onKeyDown: (e: React.KeyboardEvent) => void;\n readonly onFocus: () => void;\n readonly onBlur: () => void;\n readonly radius: number; // Radius for button positioning\n readonly buttonSize: number;\n}\n\n/**\n * D-Pad button positioned around the center\n * Memoized to prevent unnecessary re-renders\n */\nconst DPadButton = React.memo<DPadButtonProps>(\n ({\n config,\n active,\n focused,\n onStart,\n onEnd,\n onKeyDown,\n onFocus,\n onBlur,\n radius,\n buttonSize,\n }) => {\n const radian = (config.angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * radius;\n const y = Math.sin(radian) * radius;\n\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n const ariaLabel = createBilingualLabel(\n `이동 ${config.korean}`,\n `Move ${config.englishLabel}`,\n ).label;\n\n return (\n <button\n onTouchStart={onStart}\n onTouchEnd={onEnd}\n onMouseDown={onStart}\n onMouseUp={onEnd}\n onMouseLeave={onEnd}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n onBlur={onBlur}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${buttonSize / 2}px)`,\n width: `${buttonSize}px`,\n height: `${buttonSize}px`,\n borderRadius: \"50%\",\n background: active\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : \"rgba(0, 0, 0, 0.5)\",\n border: focused\n ? `3px solid rgb(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b})`\n : `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${active ? 1 : 0.6})`,\n fontSize: \"20px\",\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.2s ease, opacity 0.2s ease\",\n transform: active ? \"scale(1.1)\" : \"scale(1)\",\n boxShadow: focused\n ? `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : active\n ? `0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : \"none\",\n ...getFocusStyle(focused),\n }}\n aria-label={ariaLabel}\n aria-pressed={active}\n role=\"button\"\n tabIndex={0}\n data-testid={`dpad-button-${config.direction}`}\n >\n {config.korean}\n </button>\n );\n },\n (prevProps, nextProps) => {\n return (\n prevProps.active === nextProps.active &&\n prevProps.focused === nextProps.focused\n );\n },\n);\n\nDPadButton.displayName = \"DPadButton\";\n\n/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for touch-based movement control\n * Features:\n * - 8 directional buttons arranged in a circle\n * - Visual feedback on active direction\n * - Haptic feedback on touch\n * - Korean theming with cyberpunk aesthetics\n * - 48x48px minimum touch targets (improved from iOS 44px guideline)\n * - 140x140px default size for better mobile usability\n *\n * Usage in Combat/Training:\n * - Provides tactical positioning and footwork\n * - Alternative to keyboard WASD controls\n * - Essential for mobile gameplay\n *\n * @example\n * ```tsx\n * <VirtualDPad\n * onMove={(direction, eventType) => {\n * if (eventType === 'start' && direction) {\n * handleMovement(direction);\n * } else if (eventType === 'end') {\n * stopMovement();\n * }\n * }}\n * disabled={isPaused}\n * size={140}\n * bottom={34}\n * />\n * ```\n *\n * @korean 가상방향패드\n */\nconst VirtualDPadComponent: React.FC<VirtualDPadProps> = ({\n onMove,\n disabled = false,\n size = 140,\n bottom = 34,\n left = 20,\n opacity = 0.8,\n}) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [focusedDirection, setFocusedDirection] = useState<Direction | null>(\n null,\n );\n\n const throttledOnMove = useThrottle(onMove, 16);\n\n /**\n * Handle touch or mouse start on a direction button\n * Throttled for performance\n */\n const handleStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle touch or mouse end\n * Throttled for performance\n */\n const handleEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle keyboard navigation for D-Pad\n */\n const handleKeyDown = useCallback(\n (direction: Direction) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n setTimeout(() => {\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n }, 150);\n },\n });\n },\n [disabled, throttledOnMove],\n );\n\n const dimensions = useMemo(\n () => ({\n buttonSize: Math.max(48, size * 0.3),\n radius: (size - Math.max(48, size * 0.3)) / 2,\n }),\n [size],\n );\n\n const colors = useMemo(\n () => ({\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n left: `${left}px`,\n width: `${size}px`,\n height: `${size}px`,\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"virtual-dpad\"\n >\n {/* D-Pad Container */}\n <div\n style={{\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n background: \"rgba(0, 0, 0, 0.5)\",\n borderRadius: \"50%\",\n border: `2px solid rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.8)`,\n boxShadow: `0 0 20px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.3)`,\n }}\n role=\"group\"\n aria-label={\n createBilingualLabel(\"방향 패드\", \"Directional Pad\").label\n }\n >\n {/* Directional Buttons */}\n {DIRECTIONS.map((config) => (\n <DPadButton\n key={config.direction}\n config={config}\n active={activeDirection === config.direction}\n focused={focusedDirection === config.direction}\n onStart={(e) => handleStart(e, config.direction)}\n onEnd={handleEnd}\n onKeyDown={handleKeyDown(config.direction)}\n onFocus={() => setFocusedDirection(config.direction)}\n onBlur={() => setFocusedDirection(null)}\n radius={dimensions.radius}\n buttonSize={dimensions.buttonSize}\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 ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`\n : `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.7)`,\n border: \"2px solid #fff\",\n transition: \"transform 0.15s ease, opacity 0.15s ease\",\n boxShadow: activeDirection\n ? `0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.8)`\n : \"none\",\n }}\n data-testid=\"dpad-center\"\n />\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized VirtualDPad with custom comparison\n * Only re-renders when props change\n */\nexport const VirtualDPad = React.memo(\n VirtualDPadComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.size === nextProps.size &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.left === nextProps.left &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onMove === nextProps.onMove\n );\n },\n);\n\nVirtualDPad.displayName = \"VirtualDPad\";\n"],"mappings":"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"}
|
|
@@ -379,8 +379,7 @@ var calculateMuscleScaleFactor = (muscleMass) => {
|
|
|
379
379
|
* @korean 지방층투명도계산
|
|
380
380
|
*/
|
|
381
381
|
var calculateFatLayerOpacity = (fatMass) => {
|
|
382
|
-
const
|
|
383
|
-
const normalizedFat = (fatMass - minFat) / (22 - minFat);
|
|
382
|
+
const normalizedFat = (fatMass - 10) / 12;
|
|
384
383
|
return Math.max(.05, Math.min(.85, .05 + normalizedFat * .8));
|
|
385
384
|
};
|
|
386
385
|
/**
|
|
@@ -395,8 +394,7 @@ var calculateFatLayerOpacity = (fatMass) => {
|
|
|
395
394
|
* @korean 지방층두께계산
|
|
396
395
|
*/
|
|
397
396
|
var calculateFatLayerThickness = (fatMass) => {
|
|
398
|
-
const
|
|
399
|
-
const normalizedFat = (fatMass - minFat) / (22 - minFat);
|
|
397
|
+
const normalizedFat = (fatMass - 10) / 12;
|
|
400
398
|
return Math.max(.02, Math.min(.6, .02 + Math.pow(normalizedFat, 1.5) * .58));
|
|
401
399
|
};
|
|
402
400
|
/**
|
|
@@ -524,10 +522,11 @@ var BoneMuscles = ({ boneName, muscleStates, isExhausted, physicalAttributes })
|
|
|
524
522
|
if (!attachments || attachments.length === 0) return null;
|
|
525
523
|
return /* @__PURE__ */ jsx(Fragment, { children: attachments.map((attachment) => {
|
|
526
524
|
const tension = muscleStates.get(attachment.name) ?? 0;
|
|
525
|
+
const isShaking = isExhausted && tension > DEFAULT_MUSCLE_CONFIG.shakingTensionThreshold;
|
|
527
526
|
return /* @__PURE__ */ jsx(BoneAttachedMuscle, {
|
|
528
527
|
attachment,
|
|
529
528
|
tension,
|
|
530
|
-
isShaking
|
|
529
|
+
isShaking,
|
|
531
530
|
muscleScaleFactor,
|
|
532
531
|
fatLayerOpacity,
|
|
533
532
|
fatLayerThickness
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BoneAttachedMuscles.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/BoneAttachedMuscles.tsx"],"sourcesContent":["/* eslint-disable react-refresh/only-export-components */\n/**\n * Bone-attached muscle system for realistic muscle movement with skeleton\n *\n * Muscles are rendered as children of their parent bones, inheriting bone\n * transformations automatically for proper movement during animation.\n *\n * @module components/three/BoneAttachedMuscles\n * @category 3D Components\n * @korean 뼈부착근육시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport {\n ABS_LENGTH,\n ABS_RADIUS,\n BICEP_LENGTH,\n BICEP_RADIUS,\n CALF_LENGTH,\n CALF_RADIUS,\n CORE_LENGTH,\n CORE_RADIUS,\n ERECTOR_SPINAE_LENGTH,\n ERECTOR_SPINAE_RADIUS,\n FOREARM_LENGTH,\n FOREARM_RADIUS,\n GLUTE_LENGTH,\n GLUTE_RADIUS,\n HAMSTRING_LENGTH,\n HAMSTRING_RADIUS,\n HIP_FLEXOR_LENGTH,\n HIP_FLEXOR_RADIUS,\n LAT_LENGTH,\n LAT_RADIUS,\n OBLIQUES_LENGTH,\n OBLIQUES_RADIUS,\n PECTORALS_LENGTH,\n PECTORALS_RADIUS,\n QUAD_LENGTH,\n QUAD_RADIUS,\n SHOULDER_LENGTH,\n SHOULDER_RADIUS,\n TRAPEZIUS_LENGTH,\n TRAPEZIUS_RADIUS,\n TRICEP_LENGTH,\n TRICEP_RADIUS,\n} from \"../../../../constants/bodyDimensions\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport type { MuscleGroupName } from \"../../../../types/muscle\";\nimport { DEFAULT_MUSCLE_CONFIG } from \"../../../../types/muscle\";\n\n/**\n * Muscle attachment configuration\n *\n * Defines how a muscle is positioned relative to its parent bone.\n * Position is in local bone space, so muscles move with the bone.\n *\n * @korean 근육부착설정\n */\nexport interface MuscleAttachment {\n /** Muscle group name */\n readonly name: MuscleGroupName;\n /** Korean name for UI */\n readonly korean: string;\n /** English name for UI */\n readonly english: string;\n /** Local position offset from bone origin */\n readonly localOffset: THREE.Vector3;\n /** Local rotation relative to bone */\n readonly localRotation: THREE.Euler;\n /** Base scale when relaxed */\n readonly baseScale: THREE.Vector3;\n /** Max scale when fully flexed */\n readonly maxFlexScale: THREE.Vector3;\n /** Capsule geometry radius */\n readonly radius: number;\n /** Capsule geometry length */\n readonly length: number;\n}\n\n/**\n * Mapping of bone names to their attached muscles\n *\n * Each bone can have multiple muscles attached to it.\n * Positions are relative to the bone's local coordinate system.\n *\n * @korean 뼈근육매핑\n */\nexport const BONE_MUSCLE_MAP: Record<string, MuscleAttachment[]> = {\n shoulder_L: [\n {\n name: \"SHOULDER_L\",\n korean: \"왼쪽어깨\",\n english: \"Left Shoulder\",\n localOffset: new THREE.Vector3(-0.04, 0, 0),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.15, 1.15),\n radius: SHOULDER_RADIUS,\n length: SHOULDER_LENGTH,\n },\n ],\n shoulder_R: [\n {\n name: \"SHOULDER_R\",\n korean: \"오른쪽어깨\",\n english: \"Right Shoulder\",\n localOffset: new THREE.Vector3(0.04, 0, 0),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.15, 1.15),\n radius: SHOULDER_RADIUS,\n length: SHOULDER_LENGTH,\n },\n ],\n\n upper_arm_L: [\n {\n name: \"BICEP_L\",\n korean: \"왼쪽이두근\",\n english: \"Left Bicep\",\n localOffset: new THREE.Vector3(-0.06, 0, 0.03),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.1, 1.2),\n radius: BICEP_RADIUS,\n length: BICEP_LENGTH,\n },\n {\n name: \"TRICEP_L\",\n korean: \"왼쪽삼두근\",\n english: \"Left Tricep\",\n localOffset: new THREE.Vector3(-0.06, 0, -0.03),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: TRICEP_RADIUS,\n length: TRICEP_LENGTH,\n },\n ],\n upper_arm_R: [\n {\n name: \"BICEP_R\",\n korean: \"오른쪽이두근\",\n english: \"Right Bicep\",\n localOffset: new THREE.Vector3(0.06, 0, 0.03),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.1, 1.2),\n radius: BICEP_RADIUS,\n length: BICEP_LENGTH,\n },\n {\n name: \"TRICEP_R\",\n korean: \"오른쪽삼두근\",\n english: \"Right Tricep\",\n localOffset: new THREE.Vector3(0.06, 0, -0.03),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: TRICEP_RADIUS,\n length: TRICEP_LENGTH,\n },\n ],\n\n forearm_L: [\n {\n name: \"FOREARM_L\",\n korean: \"왼쪽전완근\",\n english: \"Left Forearm\",\n localOffset: new THREE.Vector3(-0.05, 0, 0),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.05, 1.1),\n radius: FOREARM_RADIUS,\n length: FOREARM_LENGTH,\n },\n ],\n forearm_R: [\n {\n name: \"FOREARM_R\",\n korean: \"오른쪽전완근\",\n english: \"Right Forearm\",\n localOffset: new THREE.Vector3(0.05, 0, 0),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.05, 1.1),\n radius: FOREARM_RADIUS,\n length: FOREARM_LENGTH,\n },\n ],\n\n spine_middle: [\n {\n name: \"PECTORALS\",\n korean: \"대흉근\",\n english: \"Pectorals\",\n localOffset: new THREE.Vector3(0, 0.08, 0.14),\n localRotation: new THREE.Euler(Math.PI / 2, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.2, 1.15),\n radius: PECTORALS_RADIUS,\n length: PECTORALS_LENGTH,\n },\n {\n name: \"CORE\",\n korean: \"코어\",\n english: \"Core\",\n localOffset: new THREE.Vector3(0, -0.04, 0.06),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: CORE_RADIUS,\n length: CORE_LENGTH,\n },\n {\n name: \"ABS\",\n korean: \"복근\",\n english: \"Abdominals\",\n localOffset: new THREE.Vector3(0, -0.18, 0.12),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: ABS_RADIUS,\n length: ABS_LENGTH,\n },\n {\n name: \"OBLIQUES\",\n korean: \"복사근\",\n english: \"Obliques\",\n localOffset: new THREE.Vector3(0, -0.1, 0.16),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: OBLIQUES_RADIUS,\n length: OBLIQUES_LENGTH,\n },\n ],\n\n pelvis: [\n {\n name: \"HIP_FLEXOR_L\",\n korean: \"왼쪽고관절굴근\",\n english: \"Left Hip Flexor\",\n localOffset: new THREE.Vector3(-0.05, 0.02, 0.03),\n localRotation: new THREE.Euler(0.2, 0, 0.15),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: HIP_FLEXOR_RADIUS,\n length: HIP_FLEXOR_LENGTH,\n },\n {\n name: \"HIP_FLEXOR_R\",\n korean: \"오른쪽고관절굴근\",\n english: \"Right Hip Flexor\",\n localOffset: new THREE.Vector3(0.05, 0.02, 0.03),\n localRotation: new THREE.Euler(0.2, 0, -0.15),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: HIP_FLEXOR_RADIUS,\n length: HIP_FLEXOR_LENGTH,\n },\n {\n name: \"LOWER_ABS\",\n korean: \"하복근\",\n english: \"Lower Abdominals\",\n localOffset: new THREE.Vector3(0, 0.01, 0.05),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: 0.05, // Lower abs - slightly larger than hip flexor\n length: 0.14,\n },\n ],\n\n spine_lower: [\n {\n name: \"ERECTOR_SPINAE_L\",\n korean: \"왼쪽척추기립근\",\n english: \"Left Erector Spinae\",\n localOffset: new THREE.Vector3(-0.03, 0.03, -0.04),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: ERECTOR_SPINAE_RADIUS,\n length: ERECTOR_SPINAE_LENGTH,\n },\n {\n name: \"ERECTOR_SPINAE_R\",\n korean: \"오른쪽척추기립근\",\n english: \"Right Erector Spinae\",\n localOffset: new THREE.Vector3(0.03, 0.03, -0.04),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: ERECTOR_SPINAE_RADIUS,\n length: ERECTOR_SPINAE_LENGTH,\n },\n ],\n\n spine_upper: [\n {\n name: \"LAT_L\",\n korean: \"왼쪽광배근\",\n english: \"Left Latissimus\",\n localOffset: new THREE.Vector3(-0.07, -0.01, -0.03),\n localRotation: new THREE.Euler(0, 0.2, 0.3),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.2, 1.1),\n radius: LAT_RADIUS,\n length: LAT_LENGTH,\n },\n {\n name: \"LAT_R\",\n korean: \"오른쪽광배근\",\n english: \"Right Latissimus\",\n localOffset: new THREE.Vector3(0.07, -0.01, -0.03),\n localRotation: new THREE.Euler(0, -0.2, -0.3),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.2, 1.1),\n radius: LAT_RADIUS,\n length: LAT_LENGTH,\n },\n {\n name: \"TRAPEZIUS\",\n korean: \"승모근\",\n english: \"Trapezius\",\n localOffset: new THREE.Vector3(0, 0.05, -0.04),\n localRotation: new THREE.Euler(-0.25, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.15, 1.1),\n radius: TRAPEZIUS_RADIUS,\n length: TRAPEZIUS_LENGTH,\n },\n {\n name: \"RHOMBOID\",\n korean: \"능형근\",\n english: \"Rhomboid\",\n localOffset: new THREE.Vector3(0, 0.01, -0.05),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.1),\n radius: 0.04, // Rhomboid - slightly smaller than trapezius\n length: 0.14,\n },\n ],\n\n hip_L: [\n {\n name: \"GLUTE_L\",\n korean: \"왼쪽둔근\",\n english: \"Left Glute\",\n localOffset: new THREE.Vector3(-0.03, -0.02, -0.05),\n localRotation: new THREE.Euler(Math.PI / 2, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.15, 1.15),\n radius: GLUTE_RADIUS,\n length: GLUTE_LENGTH,\n },\n ],\n hip_R: [\n {\n name: \"GLUTE_R\",\n korean: \"오른쪽둔근\",\n english: \"Right Glute\",\n localOffset: new THREE.Vector3(0.03, -0.02, -0.05),\n localRotation: new THREE.Euler(Math.PI / 2, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.15, 1.15),\n radius: GLUTE_RADIUS,\n length: GLUTE_LENGTH,\n },\n ],\n\n thigh_L: [\n {\n name: \"QUAD_L\",\n korean: \"왼쪽대퇴사두근\",\n english: \"Left Quadriceps\",\n localOffset: new THREE.Vector3(0, -0.08, 0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: QUAD_RADIUS,\n length: QUAD_LENGTH,\n },\n {\n name: \"HAMSTRING_L\",\n korean: \"왼쪽햄스트링\",\n english: \"Left Hamstring\",\n localOffset: new THREE.Vector3(0, -0.08, -0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.12, 1.1, 1.12),\n radius: HAMSTRING_RADIUS,\n length: HAMSTRING_LENGTH,\n },\n ],\n thigh_R: [\n {\n name: \"QUAD_R\",\n korean: \"오른쪽대퇴사두근\",\n english: \"Right Quadriceps\",\n localOffset: new THREE.Vector3(0, -0.08, 0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: QUAD_RADIUS,\n length: QUAD_LENGTH,\n },\n {\n name: \"HAMSTRING_R\",\n korean: \"오른쪽햄스트링\",\n english: \"Right Hamstring\",\n localOffset: new THREE.Vector3(0, -0.08, -0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.12, 1.1, 1.12),\n radius: HAMSTRING_RADIUS,\n length: HAMSTRING_LENGTH,\n },\n ],\n\n shin_L: [\n {\n name: \"CALF_L\",\n korean: \"왼쪽종아리\",\n english: \"Left Calf\",\n localOffset: new THREE.Vector3(0, -0.04, -0.01),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.08, 1.1),\n radius: CALF_RADIUS,\n length: CALF_LENGTH,\n },\n ],\n shin_R: [\n {\n name: \"CALF_R\",\n korean: \"오른쪽종아리\",\n english: \"Right Calf\",\n localOffset: new THREE.Vector3(0, -0.04, -0.01),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.08, 1.1),\n radius: CALF_RADIUS,\n length: CALF_LENGTH,\n },\n ],\n};\n\nimport {\n MIN_MUSCLE_SCALE,\n MUSCLE_AMPLIFICATION_BASE,\n MUSCLE_AMPLIFICATION_EXPONENT,\n MUSCLE_GEOMETRY_NORMALIZATION,\n REFERENCE_MUSCLE_MASS,\n} from \"../../../../constants/bodyRenderingConstants\";\n\n/**\n * Calculate muscle scale factor based on muscle mass with proportional scaling.\n *\n * Linear scaling for realistic visual differences:\n * - 28kg (Hacker) → 0.84 scale (lean, defined)\n * - 30kg (Amsalja) → 0.89 scale (lean athlete)\n * - 32kg (Jeongbo) → 0.93 scale (fit operative)\n * - 35kg (Musa) → 1.0 scale (reference baseline)\n * - 48kg (Jojik) → 1.30 scale (massive, powerful)\n *\n * @param muscleMass - Muscle mass in kilograms (archetype range: 28-48kg)\n * @returns Scale factor for muscle geometry\n *\n * @korean 근육크기계산\n */\nexport const calculateMuscleScaleFactor = (muscleMass: number): number => {\n const massRatio = muscleMass / REFERENCE_MUSCLE_MASS;\n const deviation = massRatio - 1.0;\n\n const exponentialDeviation =\n Math.sign(deviation) *\n Math.pow(Math.abs(deviation), MUSCLE_AMPLIFICATION_EXPONENT);\n\n return Math.max(\n MIN_MUSCLE_SCALE,\n 1.0 + exponentialDeviation * MUSCLE_AMPLIFICATION_BASE,\n );\n};\n\n/**\n * Calculate fat layer opacity with expanded range for better distinction.\n *\n * Previous: 0.1-0.7 opacity (insufficient contrast)\n * New: 0.05-0.85 opacity (dramatic difference)\n *\n * @param fatMass - Fat mass in kilograms (archetype range: 10-22kg)\n * @returns Opacity value for fat layer (0.05-0.85)\n *\n * @korean 지방층투명도계산\n */\nexport const calculateFatLayerOpacity = (fatMass: number): number => {\n const minFat = 10; // Amsalja minimum\n const maxFat = 22; // Jojik maximum\n const normalizedFat = (fatMass - minFat) / (maxFat - minFat);\n\n return Math.max(0.05, Math.min(0.85, 0.05 + normalizedFat * 0.8));\n};\n\n/**\n * Calculate fat layer thickness with non-linear scaling.\n *\n * Previous: 0.05-0.45 linear\n * New: 0.02-0.60 exponential (skinny = very thin, heavy = very thick)\n *\n * @param fatMass - Fat mass in kilograms (archetype range: 10-22kg)\n * @returns Scale increase for fat layer (0.02-0.60)\n *\n * @korean 지방층두께계산\n */\nexport const calculateFatLayerThickness = (fatMass: number): number => {\n const minFat = 10;\n const maxFat = 22;\n const normalizedFat = (fatMass - minFat) / (maxFat - minFat);\n\n const exponentialFat = Math.pow(normalizedFat, 1.5);\n\n return Math.max(0.02, Math.min(0.6, 0.02 + exponentialFat * 0.58));\n};\n\n/**\n * Props for BoneAttachedMuscle component\n *\n * @korean 뼈부착근육속성\n */\nexport interface BoneAttachedMuscleProps {\n /** Muscle attachment configuration */\n readonly attachment: MuscleAttachment;\n /** Tension level (0-1) for muscle flex */\n readonly tension: number;\n /** Whether muscle is shaking (exhausted state) */\n readonly isShaking: boolean;\n /** Muscle scale factor based on archetype */\n readonly muscleScaleFactor: number;\n /** Fat layer opacity */\n readonly fatLayerOpacity: number;\n /** Fat layer thickness multiplier */\n readonly fatLayerThickness: number;\n}\n\n/**\n * Single bone-attached muscle component\n *\n * Renders a muscle mesh that follows its parent bone's transformations.\n * Includes dynamic scaling based on tension and archetype.\n *\n * @korean 뼈부착근육컴포넌트\n */\nexport const BoneAttachedMuscle: React.FC<BoneAttachedMuscleProps> = ({\n attachment,\n tension,\n isShaking,\n muscleScaleFactor,\n fatLayerOpacity,\n fatLayerThickness,\n}) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const fatMeshRef = useRef<THREE.Mesh>(null);\n\n const roundedTension = Math.round(tension * 100) / 100;\n\n const currentScale = useMemo(() => {\n const t = Math.max(0, Math.min(1, roundedTension));\n const lerp = (start: number, end: number, factor: number) =>\n start + (end - start) * factor;\n\n return new THREE.Vector3(\n lerp(attachment.baseScale.x, attachment.maxFlexScale.x, t) *\n muscleScaleFactor,\n lerp(attachment.baseScale.y, attachment.maxFlexScale.y, t) *\n muscleScaleFactor,\n lerp(attachment.baseScale.z, attachment.maxFlexScale.z, t) *\n muscleScaleFactor,\n );\n }, [attachment, roundedTension, muscleScaleFactor]);\n\n const fatScale = useMemo(() => {\n return new THREE.Vector3(\n currentScale.x * (1 + fatLayerThickness),\n currentScale.y * (1 + fatLayerThickness),\n currentScale.z * (1 + fatLayerThickness),\n );\n }, [currentScale, fatLayerThickness]);\n\n const muscleColor = useMemo(() => {\n if (isShaking) {\n return KOREAN_COLORS.MUSCLE_EXHAUSTED;\n } else if (roundedTension > 0.7) {\n return KOREAN_COLORS.MUSCLE_FLEXED;\n }\n return KOREAN_COLORS.MUSCLE_TONE;\n }, [roundedTension, isShaking]);\n\n useFrame((state) => {\n if (!meshRef.current) return;\n\n if (!isShaking) {\n meshRef.current.rotation.z = attachment.localRotation.z;\n return;\n }\n\n const shake = Math.sin(state.clock.elapsedTime * 20 * Math.PI * 2) * 0.02;\n meshRef.current.rotation.z = attachment.localRotation.z + shake;\n if (fatMeshRef.current) {\n fatMeshRef.current.rotation.z = attachment.localRotation.z + shake;\n }\n });\n\n return (\n <group\n position={\n attachment.localOffset\n .toArray()\n .map((v) => v * MUSCLE_GEOMETRY_NORMALIZATION) as [\n number,\n number,\n number,\n ]\n }\n rotation={[attachment.localRotation.x, attachment.localRotation.y, 0]}\n >\n {/* Main muscle mesh */}\n <mesh\n ref={meshRef}\n rotation={[0, 0, attachment.localRotation.z]}\n scale={currentScale.toArray()}\n castShadow\n receiveShadow\n name={`muscle-${attachment.name}`}\n >\n <capsuleGeometry\n args={[\n attachment.radius * MUSCLE_GEOMETRY_NORMALIZATION,\n attachment.length * MUSCLE_GEOMETRY_NORMALIZATION,\n 8,\n 16,\n ]}\n />\n <meshPhysicalMaterial\n color={muscleColor}\n metalness={0.02}\n roughness={0.85}\n clearcoat={0.08}\n clearcoatRoughness={0.6}\n envMapIntensity={0.3}\n sheen={0.15}\n sheenRoughness={0.8}\n sheenColor={muscleColor}\n />\n </mesh>\n\n {/* Fat layer (only visible when fat mass is significant) */}\n {fatLayerOpacity > 0.05 && (\n <mesh\n ref={fatMeshRef}\n rotation={[0, 0, attachment.localRotation.z]}\n scale={fatScale.toArray()}\n castShadow\n receiveShadow\n name={`fat-layer-${attachment.name}`}\n >\n <capsuleGeometry\n args={[\n attachment.radius * MUSCLE_GEOMETRY_NORMALIZATION,\n attachment.length * MUSCLE_GEOMETRY_NORMALIZATION,\n 8,\n 16,\n ]}\n />\n <meshStandardMaterial\n color={KOREAN_COLORS.SKIN_TONE}\n metalness={0.05}\n roughness={0.95}\n transparent={true}\n opacity={fatLayerOpacity}\n depthWrite={false}\n />\n </mesh>\n )}\n </group>\n );\n};\n\n/**\n * Props for BoneMuscles component (renders all muscles for a bone)\n *\n * @korean 뼈근육들속성\n */\nexport interface BoneMusclesProps {\n /** Bone name to look up muscles for */\n readonly boneName: string;\n /** Map of muscle name to tension level */\n readonly muscleStates: Map<string, number>;\n /** Whether character is exhausted (triggers shaking) */\n readonly isExhausted: boolean;\n /** Physical attributes for scaling */\n readonly physicalAttributes?: {\n readonly muscleMass: number;\n readonly fatMass: number;\n };\n}\n\n/**\n * Renders all muscles attached to a specific bone\n *\n * Looks up muscle attachments by bone name and renders each one.\n * Muscles inherit the bone's transformations automatically.\n *\n * @korean 뼈근육들컴포넌트\n */\nexport const BoneMuscles: React.FC<BoneMusclesProps> = ({\n boneName,\n muscleStates,\n isExhausted,\n physicalAttributes,\n}) => {\n const attachments = BONE_MUSCLE_MAP[boneName];\n\n const muscleScaleFactor = useMemo(() => {\n if (!physicalAttributes) return 1.0;\n return calculateMuscleScaleFactor(physicalAttributes.muscleMass);\n }, [physicalAttributes]);\n\n const fatLayerOpacity = useMemo(() => {\n if (!physicalAttributes) return 0.0;\n return calculateFatLayerOpacity(physicalAttributes.fatMass);\n }, [physicalAttributes]);\n\n const fatLayerThickness = useMemo(() => {\n if (!physicalAttributes) return 0.0;\n return calculateFatLayerThickness(physicalAttributes.fatMass);\n }, [physicalAttributes]);\n\n if (!attachments || attachments.length === 0) {\n return null;\n }\n\n return (\n <>\n {attachments.map((attachment) => {\n const tension = muscleStates.get(attachment.name) ?? 0;\n const isShaking =\n isExhausted &&\n tension > DEFAULT_MUSCLE_CONFIG.shakingTensionThreshold;\n\n return (\n <BoneAttachedMuscle\n key={attachment.name}\n attachment={attachment}\n tension={tension}\n isShaking={isShaking}\n muscleScaleFactor={muscleScaleFactor}\n fatLayerOpacity={fatLayerOpacity}\n fatLayerThickness={fatLayerThickness}\n />\n );\n })}\n </>\n );\n};\n\nexport default BoneMuscles;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BoneAttachedMuscles.js","names":[],"sources":["../../../../../src/components/shared/three/anatomy/BoneAttachedMuscles.tsx"],"sourcesContent":["/* eslint-disable react-refresh/only-export-components */\n/**\n * Bone-attached muscle system for realistic muscle movement with skeleton\n *\n * Muscles are rendered as children of their parent bones, inheriting bone\n * transformations automatically for proper movement during animation.\n *\n * @module components/three/BoneAttachedMuscles\n * @category 3D Components\n * @korean 뼈부착근육시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport {\n ABS_LENGTH,\n ABS_RADIUS,\n BICEP_LENGTH,\n BICEP_RADIUS,\n CALF_LENGTH,\n CALF_RADIUS,\n CORE_LENGTH,\n CORE_RADIUS,\n ERECTOR_SPINAE_LENGTH,\n ERECTOR_SPINAE_RADIUS,\n FOREARM_LENGTH,\n FOREARM_RADIUS,\n GLUTE_LENGTH,\n GLUTE_RADIUS,\n HAMSTRING_LENGTH,\n HAMSTRING_RADIUS,\n HIP_FLEXOR_LENGTH,\n HIP_FLEXOR_RADIUS,\n LAT_LENGTH,\n LAT_RADIUS,\n OBLIQUES_LENGTH,\n OBLIQUES_RADIUS,\n PECTORALS_LENGTH,\n PECTORALS_RADIUS,\n QUAD_LENGTH,\n QUAD_RADIUS,\n SHOULDER_LENGTH,\n SHOULDER_RADIUS,\n TRAPEZIUS_LENGTH,\n TRAPEZIUS_RADIUS,\n TRICEP_LENGTH,\n TRICEP_RADIUS,\n} from \"../../../../constants/bodyDimensions\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport type { MuscleGroupName } from \"../../../../types/muscle\";\nimport { DEFAULT_MUSCLE_CONFIG } from \"../../../../types/muscle\";\n\n/**\n * Muscle attachment configuration\n *\n * Defines how a muscle is positioned relative to its parent bone.\n * Position is in local bone space, so muscles move with the bone.\n *\n * @korean 근육부착설정\n */\nexport interface MuscleAttachment {\n /** Muscle group name */\n readonly name: MuscleGroupName;\n /** Korean name for UI */\n readonly korean: string;\n /** English name for UI */\n readonly english: string;\n /** Local position offset from bone origin */\n readonly localOffset: THREE.Vector3;\n /** Local rotation relative to bone */\n readonly localRotation: THREE.Euler;\n /** Base scale when relaxed */\n readonly baseScale: THREE.Vector3;\n /** Max scale when fully flexed */\n readonly maxFlexScale: THREE.Vector3;\n /** Capsule geometry radius */\n readonly radius: number;\n /** Capsule geometry length */\n readonly length: number;\n}\n\n/**\n * Mapping of bone names to their attached muscles\n *\n * Each bone can have multiple muscles attached to it.\n * Positions are relative to the bone's local coordinate system.\n *\n * @korean 뼈근육매핑\n */\nexport const BONE_MUSCLE_MAP: Record<string, MuscleAttachment[]> = {\n shoulder_L: [\n {\n name: \"SHOULDER_L\",\n korean: \"왼쪽어깨\",\n english: \"Left Shoulder\",\n localOffset: new THREE.Vector3(-0.04, 0, 0),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.15, 1.15),\n radius: SHOULDER_RADIUS,\n length: SHOULDER_LENGTH,\n },\n ],\n shoulder_R: [\n {\n name: \"SHOULDER_R\",\n korean: \"오른쪽어깨\",\n english: \"Right Shoulder\",\n localOffset: new THREE.Vector3(0.04, 0, 0),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.15, 1.15),\n radius: SHOULDER_RADIUS,\n length: SHOULDER_LENGTH,\n },\n ],\n\n upper_arm_L: [\n {\n name: \"BICEP_L\",\n korean: \"왼쪽이두근\",\n english: \"Left Bicep\",\n localOffset: new THREE.Vector3(-0.06, 0, 0.03),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.1, 1.2),\n radius: BICEP_RADIUS,\n length: BICEP_LENGTH,\n },\n {\n name: \"TRICEP_L\",\n korean: \"왼쪽삼두근\",\n english: \"Left Tricep\",\n localOffset: new THREE.Vector3(-0.06, 0, -0.03),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: TRICEP_RADIUS,\n length: TRICEP_LENGTH,\n },\n ],\n upper_arm_R: [\n {\n name: \"BICEP_R\",\n korean: \"오른쪽이두근\",\n english: \"Right Bicep\",\n localOffset: new THREE.Vector3(0.06, 0, 0.03),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.2, 1.1, 1.2),\n radius: BICEP_RADIUS,\n length: BICEP_LENGTH,\n },\n {\n name: \"TRICEP_R\",\n korean: \"오른쪽삼두근\",\n english: \"Right Tricep\",\n localOffset: new THREE.Vector3(0.06, 0, -0.03),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: TRICEP_RADIUS,\n length: TRICEP_LENGTH,\n },\n ],\n\n forearm_L: [\n {\n name: \"FOREARM_L\",\n korean: \"왼쪽전완근\",\n english: \"Left Forearm\",\n localOffset: new THREE.Vector3(-0.05, 0, 0),\n localRotation: new THREE.Euler(0, 0, Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.05, 1.1),\n radius: FOREARM_RADIUS,\n length: FOREARM_LENGTH,\n },\n ],\n forearm_R: [\n {\n name: \"FOREARM_R\",\n korean: \"오른쪽전완근\",\n english: \"Right Forearm\",\n localOffset: new THREE.Vector3(0.05, 0, 0),\n localRotation: new THREE.Euler(0, 0, -Math.PI / 2),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.05, 1.1),\n radius: FOREARM_RADIUS,\n length: FOREARM_LENGTH,\n },\n ],\n\n spine_middle: [\n {\n name: \"PECTORALS\",\n korean: \"대흉근\",\n english: \"Pectorals\",\n localOffset: new THREE.Vector3(0, 0.08, 0.14),\n localRotation: new THREE.Euler(Math.PI / 2, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.2, 1.15),\n radius: PECTORALS_RADIUS,\n length: PECTORALS_LENGTH,\n },\n {\n name: \"CORE\",\n korean: \"코어\",\n english: \"Core\",\n localOffset: new THREE.Vector3(0, -0.04, 0.06),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: CORE_RADIUS,\n length: CORE_LENGTH,\n },\n {\n name: \"ABS\",\n korean: \"복근\",\n english: \"Abdominals\",\n localOffset: new THREE.Vector3(0, -0.18, 0.12),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: ABS_RADIUS,\n length: ABS_LENGTH,\n },\n {\n name: \"OBLIQUES\",\n korean: \"복사근\",\n english: \"Obliques\",\n localOffset: new THREE.Vector3(0, -0.1, 0.16),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: OBLIQUES_RADIUS,\n length: OBLIQUES_LENGTH,\n },\n ],\n\n pelvis: [\n {\n name: \"HIP_FLEXOR_L\",\n korean: \"왼쪽고관절굴근\",\n english: \"Left Hip Flexor\",\n localOffset: new THREE.Vector3(-0.05, 0.02, 0.03),\n localRotation: new THREE.Euler(0.2, 0, 0.15),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: HIP_FLEXOR_RADIUS,\n length: HIP_FLEXOR_LENGTH,\n },\n {\n name: \"HIP_FLEXOR_R\",\n korean: \"오른쪽고관절굴근\",\n english: \"Right Hip Flexor\",\n localOffset: new THREE.Vector3(0.05, 0.02, 0.03),\n localRotation: new THREE.Euler(0.2, 0, -0.15),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: HIP_FLEXOR_RADIUS,\n length: HIP_FLEXOR_LENGTH,\n },\n {\n name: \"LOWER_ABS\",\n korean: \"하복근\",\n english: \"Lower Abdominals\",\n localOffset: new THREE.Vector3(0, 0.01, 0.05),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: 0.05, // Lower abs - slightly larger than hip flexor\n length: 0.14,\n },\n ],\n\n spine_lower: [\n {\n name: \"ERECTOR_SPINAE_L\",\n korean: \"왼쪽척추기립근\",\n english: \"Left Erector Spinae\",\n localOffset: new THREE.Vector3(-0.03, 0.03, -0.04),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: ERECTOR_SPINAE_RADIUS,\n length: ERECTOR_SPINAE_LENGTH,\n },\n {\n name: \"ERECTOR_SPINAE_R\",\n korean: \"오른쪽척추기립근\",\n english: \"Right Erector Spinae\",\n localOffset: new THREE.Vector3(0.03, 0.03, -0.04),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.1, 1.1),\n radius: ERECTOR_SPINAE_RADIUS,\n length: ERECTOR_SPINAE_LENGTH,\n },\n ],\n\n spine_upper: [\n {\n name: \"LAT_L\",\n korean: \"왼쪽광배근\",\n english: \"Left Latissimus\",\n localOffset: new THREE.Vector3(-0.07, -0.01, -0.03),\n localRotation: new THREE.Euler(0, 0.2, 0.3),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.2, 1.1),\n radius: LAT_RADIUS,\n length: LAT_LENGTH,\n },\n {\n name: \"LAT_R\",\n korean: \"오른쪽광배근\",\n english: \"Right Latissimus\",\n localOffset: new THREE.Vector3(0.07, -0.01, -0.03),\n localRotation: new THREE.Euler(0, -0.2, -0.3),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.2, 1.1),\n radius: LAT_RADIUS,\n length: LAT_LENGTH,\n },\n {\n name: \"TRAPEZIUS\",\n korean: \"승모근\",\n english: \"Trapezius\",\n localOffset: new THREE.Vector3(0, 0.05, -0.04),\n localRotation: new THREE.Euler(-0.25, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.15, 1.1),\n radius: TRAPEZIUS_RADIUS,\n length: TRAPEZIUS_LENGTH,\n },\n {\n name: \"RHOMBOID\",\n korean: \"능형근\",\n english: \"Rhomboid\",\n localOffset: new THREE.Vector3(0, 0.01, -0.05),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.1),\n radius: 0.04, // Rhomboid - slightly smaller than trapezius\n length: 0.14,\n },\n ],\n\n hip_L: [\n {\n name: \"GLUTE_L\",\n korean: \"왼쪽둔근\",\n english: \"Left Glute\",\n localOffset: new THREE.Vector3(-0.03, -0.02, -0.05),\n localRotation: new THREE.Euler(Math.PI / 2, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.15, 1.15),\n radius: GLUTE_RADIUS,\n length: GLUTE_LENGTH,\n },\n ],\n hip_R: [\n {\n name: \"GLUTE_R\",\n korean: \"오른쪽둔근\",\n english: \"Right Glute\",\n localOffset: new THREE.Vector3(0.03, -0.02, -0.05),\n localRotation: new THREE.Euler(Math.PI / 2, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.15, 1.15),\n radius: GLUTE_RADIUS,\n length: GLUTE_LENGTH,\n },\n ],\n\n thigh_L: [\n {\n name: \"QUAD_L\",\n korean: \"왼쪽대퇴사두근\",\n english: \"Left Quadriceps\",\n localOffset: new THREE.Vector3(0, -0.08, 0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: QUAD_RADIUS,\n length: QUAD_LENGTH,\n },\n {\n name: \"HAMSTRING_L\",\n korean: \"왼쪽햄스트링\",\n english: \"Left Hamstring\",\n localOffset: new THREE.Vector3(0, -0.08, -0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.12, 1.1, 1.12),\n radius: HAMSTRING_RADIUS,\n length: HAMSTRING_LENGTH,\n },\n ],\n thigh_R: [\n {\n name: \"QUAD_R\",\n korean: \"오른쪽대퇴사두근\",\n english: \"Right Quadriceps\",\n localOffset: new THREE.Vector3(0, -0.08, 0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.15, 1.1, 1.15),\n radius: QUAD_RADIUS,\n length: QUAD_LENGTH,\n },\n {\n name: \"HAMSTRING_R\",\n korean: \"오른쪽햄스트링\",\n english: \"Right Hamstring\",\n localOffset: new THREE.Vector3(0, -0.08, -0.03),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.12, 1.1, 1.12),\n radius: HAMSTRING_RADIUS,\n length: HAMSTRING_LENGTH,\n },\n ],\n\n shin_L: [\n {\n name: \"CALF_L\",\n korean: \"왼쪽종아리\",\n english: \"Left Calf\",\n localOffset: new THREE.Vector3(0, -0.04, -0.01),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.08, 1.1),\n radius: CALF_RADIUS,\n length: CALF_LENGTH,\n },\n ],\n shin_R: [\n {\n name: \"CALF_R\",\n korean: \"오른쪽종아리\",\n english: \"Right Calf\",\n localOffset: new THREE.Vector3(0, -0.04, -0.01),\n localRotation: new THREE.Euler(0, 0, 0),\n baseScale: new THREE.Vector3(1.0, 1.0, 1.0),\n maxFlexScale: new THREE.Vector3(1.1, 1.08, 1.1),\n radius: CALF_RADIUS,\n length: CALF_LENGTH,\n },\n ],\n};\n\nimport {\n MIN_MUSCLE_SCALE,\n MUSCLE_AMPLIFICATION_BASE,\n MUSCLE_AMPLIFICATION_EXPONENT,\n MUSCLE_GEOMETRY_NORMALIZATION,\n REFERENCE_MUSCLE_MASS,\n} from \"../../../../constants/bodyRenderingConstants\";\n\n/**\n * Calculate muscle scale factor based on muscle mass with proportional scaling.\n *\n * Linear scaling for realistic visual differences:\n * - 28kg (Hacker) → 0.84 scale (lean, defined)\n * - 30kg (Amsalja) → 0.89 scale (lean athlete)\n * - 32kg (Jeongbo) → 0.93 scale (fit operative)\n * - 35kg (Musa) → 1.0 scale (reference baseline)\n * - 48kg (Jojik) → 1.30 scale (massive, powerful)\n *\n * @param muscleMass - Muscle mass in kilograms (archetype range: 28-48kg)\n * @returns Scale factor for muscle geometry\n *\n * @korean 근육크기계산\n */\nexport const calculateMuscleScaleFactor = (muscleMass: number): number => {\n const massRatio = muscleMass / REFERENCE_MUSCLE_MASS;\n const deviation = massRatio - 1.0;\n\n const exponentialDeviation =\n Math.sign(deviation) *\n Math.pow(Math.abs(deviation), MUSCLE_AMPLIFICATION_EXPONENT);\n\n return Math.max(\n MIN_MUSCLE_SCALE,\n 1.0 + exponentialDeviation * MUSCLE_AMPLIFICATION_BASE,\n );\n};\n\n/**\n * Calculate fat layer opacity with expanded range for better distinction.\n *\n * Previous: 0.1-0.7 opacity (insufficient contrast)\n * New: 0.05-0.85 opacity (dramatic difference)\n *\n * @param fatMass - Fat mass in kilograms (archetype range: 10-22kg)\n * @returns Opacity value for fat layer (0.05-0.85)\n *\n * @korean 지방층투명도계산\n */\nexport const calculateFatLayerOpacity = (fatMass: number): number => {\n const minFat = 10; // Amsalja minimum\n const maxFat = 22; // Jojik maximum\n const normalizedFat = (fatMass - minFat) / (maxFat - minFat);\n\n return Math.max(0.05, Math.min(0.85, 0.05 + normalizedFat * 0.8));\n};\n\n/**\n * Calculate fat layer thickness with non-linear scaling.\n *\n * Previous: 0.05-0.45 linear\n * New: 0.02-0.60 exponential (skinny = very thin, heavy = very thick)\n *\n * @param fatMass - Fat mass in kilograms (archetype range: 10-22kg)\n * @returns Scale increase for fat layer (0.02-0.60)\n *\n * @korean 지방층두께계산\n */\nexport const calculateFatLayerThickness = (fatMass: number): number => {\n const minFat = 10;\n const maxFat = 22;\n const normalizedFat = (fatMass - minFat) / (maxFat - minFat);\n\n const exponentialFat = Math.pow(normalizedFat, 1.5);\n\n return Math.max(0.02, Math.min(0.6, 0.02 + exponentialFat * 0.58));\n};\n\n/**\n * Props for BoneAttachedMuscle component\n *\n * @korean 뼈부착근육속성\n */\nexport interface BoneAttachedMuscleProps {\n /** Muscle attachment configuration */\n readonly attachment: MuscleAttachment;\n /** Tension level (0-1) for muscle flex */\n readonly tension: number;\n /** Whether muscle is shaking (exhausted state) */\n readonly isShaking: boolean;\n /** Muscle scale factor based on archetype */\n readonly muscleScaleFactor: number;\n /** Fat layer opacity */\n readonly fatLayerOpacity: number;\n /** Fat layer thickness multiplier */\n readonly fatLayerThickness: number;\n}\n\n/**\n * Single bone-attached muscle component\n *\n * Renders a muscle mesh that follows its parent bone's transformations.\n * Includes dynamic scaling based on tension and archetype.\n *\n * @korean 뼈부착근육컴포넌트\n */\nexport const BoneAttachedMuscle: React.FC<BoneAttachedMuscleProps> = ({\n attachment,\n tension,\n isShaking,\n muscleScaleFactor,\n fatLayerOpacity,\n fatLayerThickness,\n}) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const fatMeshRef = useRef<THREE.Mesh>(null);\n\n const roundedTension = Math.round(tension * 100) / 100;\n\n const currentScale = useMemo(() => {\n const t = Math.max(0, Math.min(1, roundedTension));\n const lerp = (start: number, end: number, factor: number) =>\n start + (end - start) * factor;\n\n return new THREE.Vector3(\n lerp(attachment.baseScale.x, attachment.maxFlexScale.x, t) *\n muscleScaleFactor,\n lerp(attachment.baseScale.y, attachment.maxFlexScale.y, t) *\n muscleScaleFactor,\n lerp(attachment.baseScale.z, attachment.maxFlexScale.z, t) *\n muscleScaleFactor,\n );\n }, [attachment, roundedTension, muscleScaleFactor]);\n\n const fatScale = useMemo(() => {\n return new THREE.Vector3(\n currentScale.x * (1 + fatLayerThickness),\n currentScale.y * (1 + fatLayerThickness),\n currentScale.z * (1 + fatLayerThickness),\n );\n }, [currentScale, fatLayerThickness]);\n\n const muscleColor = useMemo(() => {\n if (isShaking) {\n return KOREAN_COLORS.MUSCLE_EXHAUSTED;\n } else if (roundedTension > 0.7) {\n return KOREAN_COLORS.MUSCLE_FLEXED;\n }\n return KOREAN_COLORS.MUSCLE_TONE;\n }, [roundedTension, isShaking]);\n\n useFrame((state) => {\n if (!meshRef.current) return;\n\n if (!isShaking) {\n meshRef.current.rotation.z = attachment.localRotation.z;\n return;\n }\n\n const shake = Math.sin(state.clock.elapsedTime * 20 * Math.PI * 2) * 0.02;\n meshRef.current.rotation.z = attachment.localRotation.z + shake;\n if (fatMeshRef.current) {\n fatMeshRef.current.rotation.z = attachment.localRotation.z + shake;\n }\n });\n\n return (\n <group\n position={\n attachment.localOffset\n .toArray()\n .map((v) => v * MUSCLE_GEOMETRY_NORMALIZATION) as [\n number,\n number,\n number,\n ]\n }\n rotation={[attachment.localRotation.x, attachment.localRotation.y, 0]}\n >\n {/* Main muscle mesh */}\n <mesh\n ref={meshRef}\n rotation={[0, 0, attachment.localRotation.z]}\n scale={currentScale.toArray()}\n castShadow\n receiveShadow\n name={`muscle-${attachment.name}`}\n >\n <capsuleGeometry\n args={[\n attachment.radius * MUSCLE_GEOMETRY_NORMALIZATION,\n attachment.length * MUSCLE_GEOMETRY_NORMALIZATION,\n 8,\n 16,\n ]}\n />\n <meshPhysicalMaterial\n color={muscleColor}\n metalness={0.02}\n roughness={0.85}\n clearcoat={0.08}\n clearcoatRoughness={0.6}\n envMapIntensity={0.3}\n sheen={0.15}\n sheenRoughness={0.8}\n sheenColor={muscleColor}\n />\n </mesh>\n\n {/* Fat layer (only visible when fat mass is significant) */}\n {fatLayerOpacity > 0.05 && (\n <mesh\n ref={fatMeshRef}\n rotation={[0, 0, attachment.localRotation.z]}\n scale={fatScale.toArray()}\n castShadow\n receiveShadow\n name={`fat-layer-${attachment.name}`}\n >\n <capsuleGeometry\n args={[\n attachment.radius * MUSCLE_GEOMETRY_NORMALIZATION,\n attachment.length * MUSCLE_GEOMETRY_NORMALIZATION,\n 8,\n 16,\n ]}\n />\n <meshStandardMaterial\n color={KOREAN_COLORS.SKIN_TONE}\n metalness={0.05}\n roughness={0.95}\n transparent={true}\n opacity={fatLayerOpacity}\n depthWrite={false}\n />\n </mesh>\n )}\n </group>\n );\n};\n\n/**\n * Props for BoneMuscles component (renders all muscles for a bone)\n *\n * @korean 뼈근육들속성\n */\nexport interface BoneMusclesProps {\n /** Bone name to look up muscles for */\n readonly boneName: string;\n /** Map of muscle name to tension level */\n readonly muscleStates: Map<string, number>;\n /** Whether character is exhausted (triggers shaking) */\n readonly isExhausted: boolean;\n /** Physical attributes for scaling */\n readonly physicalAttributes?: {\n readonly muscleMass: number;\n readonly fatMass: number;\n };\n}\n\n/**\n * Renders all muscles attached to a specific bone\n *\n * Looks up muscle attachments by bone name and renders each one.\n * Muscles inherit the bone's transformations automatically.\n *\n * @korean 뼈근육들컴포넌트\n */\nexport const BoneMuscles: React.FC<BoneMusclesProps> = ({\n boneName,\n muscleStates,\n isExhausted,\n physicalAttributes,\n}) => {\n const attachments = BONE_MUSCLE_MAP[boneName];\n\n const muscleScaleFactor = useMemo(() => {\n if (!physicalAttributes) return 1.0;\n return calculateMuscleScaleFactor(physicalAttributes.muscleMass);\n }, [physicalAttributes]);\n\n const fatLayerOpacity = useMemo(() => {\n if (!physicalAttributes) return 0.0;\n return calculateFatLayerOpacity(physicalAttributes.fatMass);\n }, [physicalAttributes]);\n\n const fatLayerThickness = useMemo(() => {\n if (!physicalAttributes) return 0.0;\n return calculateFatLayerThickness(physicalAttributes.fatMass);\n }, [physicalAttributes]);\n\n if (!attachments || attachments.length === 0) {\n return null;\n }\n\n return (\n <>\n {attachments.map((attachment) => {\n const tension = muscleStates.get(attachment.name) ?? 0;\n const isShaking =\n isExhausted &&\n tension > DEFAULT_MUSCLE_CONFIG.shakingTensionThreshold;\n\n return (\n <BoneAttachedMuscle\n key={attachment.name}\n attachment={attachment}\n tension={tension}\n isShaking={isShaking}\n muscleScaleFactor={muscleScaleFactor}\n fatLayerOpacity={fatLayerOpacity}\n fatLayerThickness={fatLayerThickness}\n />\n );\n })}\n </>\n );\n};\n\nexport default BoneMuscles;\n"],"mappings":"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"}
|