All files / components/shared/effects ScreenFlash.tsx

73.07% Statements 38/52
68.42% Branches 13/19
55.55% Functions 5/9
72.54% Lines 37/51

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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226                                                                                                        1x         15x 15x 15x   15x 8x 1x 1x 1x       7x 7x     7x 7x 7x 7x   7x 1x 1x 1x 1x 1x   1x         6x                       6x     6x     7x     15x 8x       7x 15x 15x 15x   15x                                           1x                                                                   1x                                                                               1x       5x        
/* eslint-disable react-refresh/only-export-components */
/**
 * Screen flash effect for explosive impacts
 *
 * Provides fullscreen flash effects for Jin (Thunder) trigram techniques
 * with Korean cyberpunk color scheme.
 *
 * **Korean Martial Arts Context:**
 * - 진괘 (Jin): Thunder trigram explosive release
 * - 화면 섬광 (Screen Flash): Visual impact emphasis
 * - 폭발 효과 (Explosive Effect): Thunder burst visualization
 *
 * @module components/shared/effects/ScreenFlash
 * @korean 화면섬광
 */
 
import React, { useEffect, useState } from "react";
import { useKoreanTheme } from "../../shared/base/useKoreanTheme";
import { KOREAN_COLORS } from "@/types/constants";
 
/**
 * Screen flash configuration
 */
export interface ScreenFlashConfig {
  /** Flash intensity (0-1) */
  readonly intensity: number;
  /** Duration in milliseconds */
  readonly duration: number;
  /** Flash color (hex number) */
  readonly color?: number;
  /** Fade curve: 'linear' | 'ease-out' | 'ease-in-out' */
  readonly fadeCurve?: "linear" | "ease-out" | "ease-in-out";
}
 
/**
 * Props for ScreenFlash component
 */
export interface ScreenFlashProps {
  /** Whether flash is active */
  readonly active: boolean;
  /** Flash configuration */
  readonly config: ScreenFlashConfig;
  /** Callback when flash completes */
  readonly onComplete?: () => void;
}
 
/**
 * ScreenFlash component - Fullscreen flash overlay
 *
 * Renders a fullscreen overlay with fade-out animation for explosive impacts.
 * Uses Korean cyberpunk color palette for authentic Jin technique feel.
 */
export const ScreenFlash: React.FC<ScreenFlashProps> = ({
  active,
  config,
  onComplete,
}) => {
  const { colors } = useKoreanTheme();
  const [opacity, setOpacity] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);
 
  useEffect(() => {
    if (!active) {
      setOpacity(0);
      setIsAnimating(false);
      return;
    }
 
    // Start flash at full intensity
    setOpacity(config.intensity);
    setIsAnimating(true);
 
    // Fade out over duration
    const startTime = Date.now();
    const fadeInterval = setInterval(() => {
      const elapsed = Date.now() - startTime;
      const progress = elapsed / config.duration;
 
      if (progress >= 1) {
        setOpacity(0);
        setIsAnimating(false);
        clearInterval(fadeInterval);
        Eif (onComplete) {
          onComplete();
        }
        return;
      }
 
      // Apply fade curve
      let fadeProgress: number;
      switch (config.fadeCurve) {
        case "ease-out":
          fadeProgress = 1 - Math.pow(1 - progress, 3);
          break;
        case "ease-in-out":
          fadeProgress =
            progress < 0.5
              ? 2 * progress * progress
              : 1 - Math.pow(-2 * progress + 2, 2) / 2;
          break;
        default:
          // linear
          fadeProgress = progress;
      }
 
      setOpacity(config.intensity * (1 - fadeProgress));
    }, 16); // ~60fps
 
    return () => clearInterval(fadeInterval);
  }, [active, config, onComplete]);
 
  if (!isAnimating && opacity === 0) {
    return null;
  }
 
  // Convert hex color to RGB
  const color = config.color ?? colors.ACCENT_GOLD;
  const r = (color >> 16) & 255;
  const g = (color >> 8) & 255;
  const b = color & 255;
 
  return (
    <div
      data-testid="screen-flash"
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: `rgb(${r}, ${g}, ${b})`,
        opacity,
        pointerEvents: "none",
        zIndex: 9999,
        transition: "opacity 0.016s linear",
      }}
    />
  );
};
 
/**
 * Predefined flash profiles for Jin techniques
 */
export const JIN_FLASH_PROFILES = {
  /** Light flash for quick strikes */
  light: {
    intensity: 0.4,
    duration: 150,
    color: KOREAN_COLORS.PRIMARY_CYAN,
    fadeCurve: "ease-out" as const,
  },
  /** Medium flash for standard impacts */
  medium: {
    intensity: 0.5,
    duration: 200,
    color: KOREAN_COLORS.ACCENT_GOLD,
    fadeCurve: "ease-out" as const,
  },
  /** Heavy flash for powerful techniques */
  heavy: {
    intensity: 0.6,
    duration: 250,
    color: KOREAN_COLORS.ACCENT_RED,
    fadeCurve: "ease-out" as const,
  },
  /** Explosive flash for maximum impact */
  explosive: {
    intensity: 0.8,
    duration: 300,
    color: KOREAN_COLORS.ACCENT_GOLD,
    fadeCurve: "ease-in-out" as const,
  },
} as const;
 
/**
 * React hook for screen flash effects
 */
export const useScreenFlash = () => {
  const [flashConfig, setFlashConfig] = useState<ScreenFlashConfig | null>(
    null,
  );
  const [isActive, setIsActive] = useState(false);
 
  const trigger = (config: ScreenFlashConfig) => {
    setFlashConfig(config);
    setIsActive(true);
  };
 
  const triggerProfile = (profile: keyof typeof JIN_FLASH_PROFILES) => {
    trigger(JIN_FLASH_PROFILES[profile]);
  };
 
  const handleComplete = () => {
    setIsActive(false);
  };
 
  return {
    /** Trigger flash with custom configuration */
    flash: trigger,
    /** Trigger flash using predefined profile */
    flashProfile: triggerProfile,
    /** Flash component to render */
    FlashComponent: flashConfig ? (
      <ScreenFlash
        active={isActive}
        config={flashConfig}
        onComplete={handleComplete}
      />
    ) : null,
    /** Whether flash is currently active */
    isActive,
  };
};
 
/**
 * Calculate flash intensity from Jin technique properties
 */
export const calculateJinFlashIntensity = (
  techniqueIntensity: number,
  explosivePower: number,
): number => {
  return Math.min(1.0, techniqueIntensity * explosivePower * 0.6);
};
 
export default ScreenFlash;