All files / web/src/arcade-games/know-your-world/features/reveal useGiveUpReveal.ts

0% Statements 0/240
0% Branches 0/1
0% Functions 0/1
0% Lines 0/240

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 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
/**
 * Give Up Reveal Animation Hook
 *
 * Manages the zoom and pulsing animation effect when the player gives up
 * and the target region is revealed on the map.
 *
 * Features:
 * - Zooms in on the revealed region using CSS transforms
 * - Pulsing flash animation to highlight the region
 * - Smooth zoom-out after animation completes
 * - Saves button position to prevent layout jumping during zoom
 *
 * Usage:
 * ```tsx
 * const {
 *   giveUpFlashProgress,
 *   isGiveUpAnimating,
 *   giveUpZoomTarget,
 *   savedButtonPosition,
 * } = useGiveUpReveal({
 *   giveUpReveal: props.giveUpReveal,
 *   svgRef,
 *   containerRef,
 *   fillContainer,
 * })
 * ```
 */

'use client'

import { useEffect, useState } from 'react'
import { usePulsingAnimation } from '../animations'

// ============================================================================
// Types
// ============================================================================

export interface GiveUpReveal {
  regionId: string
  regionName: string
  timestamp: number
}

export interface ZoomTarget {
  scale: number
  translateX: number
  translateY: number
}

export interface ButtonPosition {
  top: number
  right: number
}

export interface UseGiveUpRevealOptions {
  /** The give-up reveal state, or null if not revealing */
  giveUpReveal: GiveUpReveal | null
  /** Reference to the SVG element */
  svgRef: React.RefObject<SVGSVGElement | null>
  /** Reference to the container element */
  containerRef: React.RefObject<HTMLDivElement | null>
  /** Whether the map fills the container (affects nav offset) */
  fillContainer: boolean
  /** Nav height offset for full-viewport mode (default: 150) */
  navHeightOffset?: number
}

export interface UseGiveUpRevealReturn {
  /** Pulsing value 0-1 for flash animation */
  giveUpFlashProgress: number
  /** Whether animation is currently in progress */
  isGiveUpAnimating: boolean
  /** CSS transform target values for zoom animation */
  giveUpZoomTarget: ZoomTarget
  /** Saved button position to prevent jumping during zoom */
  savedButtonPosition: ButtonPosition | null
}

// ============================================================================
// Constants
// ============================================================================

/** Duration of the give-up animation in milliseconds */
const GIVE_UP_ANIMATION_DURATION = 2000

/** Number of pulses during the give-up animation */
const GIVE_UP_ANIMATION_PULSES = 3

/** Default nav height offset */
const DEFAULT_NAV_HEIGHT_OFFSET = 150

/** Default zoom target (no transformation) */
const DEFAULT_ZOOM_TARGET: ZoomTarget = {
  scale: 1,
  translateX: 0,
  translateY: 0,
}

// ============================================================================
// Hook Implementation
// ============================================================================

/**
 * Hook for managing give-up reveal animation.
 *
 * When giveUpReveal changes (detected by timestamp), triggers a zoom-in
 * animation to the revealed region with a pulsing flash effect.
 *
 * @param options - Configuration options
 * @returns Give-up reveal animation state
 */
export function useGiveUpReveal(options: UseGiveUpRevealOptions): UseGiveUpRevealReturn {
  const {
    giveUpReveal,
    svgRef,
    containerRef,
    fillContainer,
    navHeightOffset = DEFAULT_NAV_HEIGHT_OFFSET,
  } = options

  // Animation state
  const [giveUpFlashProgress, setGiveUpFlashProgress] = useState(0)
  const [isGiveUpAnimating, setIsGiveUpAnimating] = useState(false)
  const [giveUpZoomTarget, setGiveUpZoomTarget] = useState<ZoomTarget>(DEFAULT_ZOOM_TARGET)
  const [savedButtonPosition, setSavedButtonPosition] = useState<ButtonPosition | null>(null)

  // Animation controller
  const giveUpAnimation = usePulsingAnimation()

  // Give up reveal animation effect
  useEffect(() => {
    if (!giveUpReveal) {
      setGiveUpFlashProgress(0)
      setIsGiveUpAnimating(false)
      setSavedButtonPosition(null)
      // Reset transform to default when animation clears
      setGiveUpZoomTarget(DEFAULT_ZOOM_TARGET)
      return
    }

    // Track if this effect has been cleaned up (prevents stale animations)
    let isCancelled = false
    let timeoutId: ReturnType<typeof setTimeout> | null = null

    // Start animation
    setIsGiveUpAnimating(true)

    // Save current button position before zoom changes the layout
    if (svgRef.current && containerRef.current) {
      const svgRect = svgRef.current.getBoundingClientRect()
      const containerRect = containerRef.current.getBoundingClientRect()
      const svgOffsetX = svgRect.left - containerRect.left
      const svgOffsetY = svgRect.top - containerRect.top
      // Add nav offset when in full-viewport mode
      const buttonTop = svgOffsetY + 8 + (fillContainer ? navHeightOffset : 0)
      const buttonRight = containerRect.width - (svgOffsetX + svgRect.width) + 8
      setSavedButtonPosition({ top: buttonTop, right: buttonRight })
    }

    // Calculate CSS transform to zoom and center on the revealed region
    if (svgRef.current && containerRef.current) {
      const path = svgRef.current.querySelector(`path[data-region-id="${giveUpReveal.regionId}"]`)
      if (path && path instanceof SVGGeometryElement) {
        const bbox = path.getBoundingClientRect()
        const svgRect = svgRef.current.getBoundingClientRect()

        // Calculate CSS transform for zoom animation
        // Region center relative to SVG element
        const regionCenterX = bbox.left + bbox.width / 2 - svgRect.left
        const regionCenterY = bbox.top + bbox.height / 2 - svgRect.top

        // SVG center
        const svgCenterX = svgRect.width / 2
        const svgCenterY = svgRect.height / 2

        // Calculate scale: zoom in so region is clearly visible
        // For tiny regions, zoom more; for larger ones, zoom less
        const regionSize = Math.max(bbox.width, bbox.height)
        const targetSize = Math.min(svgRect.width, svgRect.height) * 0.3 // Region should be ~30% of viewport
        const scale = Math.min(8, Math.max(2, targetSize / Math.max(regionSize, 1)))

        // Calculate translation to center the region
        // After scaling, we need to translate so the region center is at SVG center
        const translateX = (svgCenterX - regionCenterX) * scale
        const translateY = (svgCenterY - regionCenterY) * scale

        // Start zoom-in animation using CSS transform
        setGiveUpZoomTarget({ scale, translateX, translateY })
      }
    }

    // Animation: 3 pulses over 2 seconds using shared pulsing hook
    giveUpAnimation.start({
      duration: GIVE_UP_ANIMATION_DURATION,
      pulses: GIVE_UP_ANIMATION_PULSES,
      onProgress: (pulseProgress) => {
        if (!isCancelled) {
          setGiveUpFlashProgress(pulseProgress)
        }
      },
      onComplete: () => {
        if (isCancelled) return
        // Animation complete - zoom back out to default
        setGiveUpZoomTarget(DEFAULT_ZOOM_TARGET)

        // Clear reveal state after a short delay to let zoom-out start
        timeoutId = setTimeout(() => {
          if (!isCancelled) {
            setGiveUpFlashProgress(0)
            setIsGiveUpAnimating(false)
            setSavedButtonPosition(null)
          }
        }, 100)
      },
    })

    // Cleanup: cancel animation if giveUpReveal changes before animation completes
    return () => {
      isCancelled = true
      giveUpAnimation.cancel()
      if (timeoutId !== null) {
        clearTimeout(timeoutId)
      }
    }
  }, [
    giveUpReveal?.timestamp,
    giveUpAnimation,
    svgRef,
    containerRef,
    fillContainer,
    navHeightOffset,
  ])

  return {
    giveUpFlashProgress,
    isGiveUpAnimating,
    giveUpZoomTarget,
    savedButtonPosition,
  }
}