All files / web/src/arcade-games/know-your-world/features/animations usePulsingAnimation.ts

43.67% Statements 38/87
100% Branches 0/0
0% Functions 0/1
43.67% Lines 38/87

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 881x 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                                                                                                    
import { useCallback, useMemo, useRef } from 'react'
 
export interface PulsingAnimationOptions {
  /** Duration of the animation in milliseconds */
  duration: number
  /** Number of pulses during the animation */
  pulses: number
  /** Called on each animation frame with the pulse progress (0-1) */
  onProgress: (pulseProgress: number) => void
  /** Called when the animation completes naturally (not cancelled) */
  onComplete?: () => void
}
 
/**
 * Hook for creating pulsing animations using requestAnimationFrame.
 *
 * The pulsing effect uses a sine wave for smooth on/off transitions:
 * `sin(progress * π * pulses * 2) * 0.5 + 0.5`
 *
 * This creates a value that oscillates between 0 and 1 for the specified
 * number of pulses over the duration.
 *
 * @example
 * ```tsx
 * const { start, cancel } = usePulsingAnimation()
 *
 * useEffect(() => {
 *   start({
 *     duration: 2000,
 *     pulses: 3,
 *     onProgress: (progress) => setFlashProgress(progress),
 *     onComplete: () => console.log('Animation complete')
 *   })
 *   return () => cancel()
 * }, [start, cancel])
 * ```
 */
export function usePulsingAnimation() {
  const animationFrameId = useRef<number | null>(null)
  const isCancelled = useRef(false)

  const cancel = useCallback(() => {
    isCancelled.current = true
    if (animationFrameId.current !== null) {
      cancelAnimationFrame(animationFrameId.current)
      animationFrameId.current = null
    }
  }, [])

  const start = useCallback(
    (options: PulsingAnimationOptions) => {
      const { duration, pulses, onProgress, onComplete } = options

      // Cancel any existing animation
      cancel()

      // Reset cancellation flag for new animation
      isCancelled.current = false

      const startTime = Date.now()

      const animate = () => {
        if (isCancelled.current) return

        const elapsed = Date.now() - startTime
        const progress = Math.min(elapsed / duration, 1)

        // Create pulsing effect: sin wave for smooth on/off
        const pulseProgress = Math.sin(progress * Math.PI * pulses * 2) * 0.5 + 0.5
        onProgress(pulseProgress)

        if (progress < 1) {
          animationFrameId.current = requestAnimationFrame(animate)
        } else {
          animationFrameId.current = null
          onComplete?.()
        }
      }

      animationFrameId.current = requestAnimationFrame(animate)
    },
    [cancel]
  )

  // Memoize the returned object to prevent re-renders when used as a dependency
  return useMemo(() => ({ start, cancel }), [start, cancel])
}