All files / components/three StanceSymbol3D.tsx

12.5% Statements 2/16
0% Branches 0/10
0% Functions 0/5
16.66% Lines 2/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167                                                                              3x                                                     3x                                                                                                                                                                                                        
/**
 * StanceSymbol3D - Floating trigram symbol above player
 * 
 * Displays the Unicode trigram symbol (☰☱☲☳☴☵☶☷) floating above the player's head,
 * with rotation animation and pulsing glow effect. Provides immediate visual feedback
 * of the current stance to the player and observers.
 * 
 * @module components/three/StanceSymbol3D
 * @category 3D Components
 * @korean 자세기호3D
 */
 
import { Html } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import React, { useRef, useMemo } from "react";
import * as THREE from "three";
import { TrigramStance } from "../../types/common";
import { FONT_FAMILY } from "../../types/constants";
import { getTrigramSymbol, getStanceKoreanName, getStanceColorHex } from "../../utils/stanceHelpers";
 
/**
 * Props for StanceSymbol3D component
 */
export interface StanceSymbol3DProps {
  /** Current trigram stance to display */
  readonly stance: TrigramStance;
  /** Height offset above player (default: 2.5) */
  readonly heightOffset?: number;
  /** Whether symbol should rotate */
  readonly animated?: boolean;
  /** Symbol scale multiplier */
  readonly scale?: number;
  /** Show Korean name below symbol */
  readonly showName?: boolean;
}
 
/**
 * Animation constants for stance symbol
 */
const ANIMATION_CONSTANTS = {
  ROTATION_SPEED: 0.5,
  BOB_AMPLITUDE: 0.1,
  BOB_FREQUENCY: 2,
} as const;
 
/**
 * StanceSymbol3D Component
 * 
 * Renders a floating trigram symbol above the player with:
 * - Rotation animation
 * - Pulsing glow effect
 * - Stance-specific coloring
 * - Optional Korean name display
 * 
 * Uses Html from @react-three/drei for crisp text rendering that always faces camera.
 * 
 * @example
 * ```tsx
 * <StanceSymbol3D 
 *   stance={TrigramStance.GEON}
 *   heightOffset={2.5}
 *   animated={true}
 *   showName={true}
 * />
 * ```
 */
export const StanceSymbol3D: React.FC<StanceSymbol3DProps> = ({
  stance,
  heightOffset = 2.5,
  animated = true,
  scale = 1.0,
  showName = true,
}) => {
  const groupRef = useRef<THREE.Group>(null);
  
  // Get stance properties
  const symbol = useMemo(() => getTrigramSymbol(stance), [stance]);
  const koreanName = useMemo(() => getStanceKoreanName(stance), [stance]);
  const colorHex = useMemo(() => getStanceColorHex(stance), [stance]);
 
  // Animation loop - rotation and pulse
  useFrame((state) => {
    if (!animated || !groupRef.current) return;
 
    const time = state.clock.elapsedTime;
 
    // Rotate symbol slowly
    groupRef.current.rotation.y = time * ANIMATION_CONSTANTS.ROTATION_SPEED;
 
    // Gentle vertical bob - oscillate around 0 (group is already positioned at heightOffset)
    groupRef.current.position.y = Math.sin(time * ANIMATION_CONSTANTS.BOB_FREQUENCY) * ANIMATION_CONSTANTS.BOB_AMPLITUDE;
  });
 
  return (
    <group ref={groupRef} position={[0, heightOffset, 0]} data-testid="stance-symbol-3d">
      {/* Trigram symbol with glow effect */}
      <Html
        center
        distanceFactor={10}
        zIndexRange={[100, 0]}
        style={{
          pointerEvents: 'none',
          userSelect: 'none',
        }}
      >
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            gap: '4px',
          }}
        >
          {/* Main trigram symbol */}
          <div
            style={{
              fontSize: `${48 * scale}px`,
              fontFamily: FONT_FAMILY.KOREAN,
              color: colorHex,
              textShadow: `
                0 0 10px ${colorHex},
                0 0 20px ${colorHex},
                0 0 30px ${colorHex}
              `,
              fontWeight: 'bold',
              lineHeight: '1',
              animation: 'pulse 2s ease-in-out infinite',
            }}
            data-testid="trigram-symbol"
          >
            {symbol}
          </div>
          
          {/* Korean name below symbol */}
          {showName && (
            <div
              style={{
                fontSize: `${16 * scale}px`,
                fontFamily: FONT_FAMILY.KOREAN,
                color: colorHex,
                textShadow: `0 0 5px ${colorHex}`,
                fontWeight: 'bold',
                letterSpacing: '2px',
              }}
              data-testid="stance-korean-name"
            >
              {koreanName}
            </div>
          )}
        </div>
 
        {/* CSS animation for pulse effect */}
        <style>
          {`
            @keyframes pulse {
              0%, 100% { opacity: 1; transform: scale(1); }
              50% { opacity: 0.8; transform: scale(1.1); }
            }
          `}
        </style>
      </Html>
    </group>
  );
};
 
export default StanceSymbol3D;