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, } } |