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 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* 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,
}
}
|