All files / web/src/components/toys/number-line/primes renderTourSpotlight.ts

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

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                                                                                                                                                                                                     
import type { NumberLineState } from '../types'
import { numberToScreenX } from '../numberLineTicks'
import { primeColorRgba } from './primeColors'
import { smallestPrimeFactor } from './sieve'

const TAU = Math.PI * 2
const SPOTLIGHT_RADIUS = 40
const FEATHER_WIDTH = 20

/**
 * Render a dim overlay with spotlight cutouts around highlighted values,
 * plus a pulsing glow on each highlighted number.
 *
 * Called after renderNumberLine() during the prime tour to direct attention
 * to the numbers the narrator is describing.
 */
export function renderTourSpotlight(
  ctx: CanvasRenderingContext2D,
  state: NumberLineState,
  cssWidth: number,
  cssHeight: number,
  isDark: boolean,
  highlightValues: number[],
  dimAmount: number,
  tourOpacity: number
): void {
  if (highlightValues.length === 0 || dimAmount <= 0 || tourOpacity <= 0) return

  const centerY = cssHeight / 2
  const dimAlpha = dimAmount * tourOpacity

  // Compute screen positions for each highlight
  const spots = highlightValues.map((v) => ({
    value: v,
    screenX: numberToScreenX(v, state.center, state.pixelsPerUnit, cssWidth),
  }))

  // --- 1. Dim overlay with spotlight cutouts (even-odd fill) ---
  ctx.save()
  ctx.beginPath()
  // Outer rect (clockwise)
  ctx.rect(0, 0, cssWidth, cssHeight)
  // Spotlight holes (counter-clockwise)
  for (const spot of spots) {
    ctx.moveTo(spot.screenX + SPOTLIGHT_RADIUS, centerY)
    ctx.arc(spot.screenX, centerY, SPOTLIGHT_RADIUS, 0, TAU, true)
  }
  ctx.fillStyle = isDark ? `rgba(0, 0, 0, ${dimAlpha})` : `rgba(0, 0, 0, ${dimAlpha * 0.7})`
  ctx.fill('evenodd')
  ctx.restore()

  // --- 2. Feathered edges around each cutout ---
  for (const spot of spots) {
    const innerR = SPOTLIGHT_RADIUS
    const outerR = SPOTLIGHT_RADIUS + FEATHER_WIDTH
    const gradient = ctx.createRadialGradient(
      spot.screenX,
      centerY,
      innerR,
      spot.screenX,
      centerY,
      outerR
    )
    gradient.addColorStop(0, 'rgba(0, 0, 0, 0)')
    const featherAlpha = isDark ? dimAlpha : dimAlpha * 0.7
    gradient.addColorStop(1, `rgba(0, 0, 0, ${featherAlpha})`)

    ctx.save()
    ctx.fillStyle = gradient
    ctx.fillRect(spot.screenX - outerR, centerY - outerR, outerR * 2, outerR * 2)
    ctx.restore()
  }

  // --- 3. Pulsing glow at each highlighted position ---
  const pulsePhase = (Date.now() % 2000) / 2000
  const pulseAlpha = 0.15 + 0.1 * Math.sin(pulsePhase * TAU)

  for (const spot of spots) {
    const spf = spot.value >= 2 ? smallestPrimeFactor(spot.value) : spot.value
    const glowRadius = 30
    const glow = ctx.createRadialGradient(
      spot.screenX,
      centerY,
      0,
      spot.screenX,
      centerY,
      glowRadius
    )
    glow.addColorStop(0, primeColorRgba(spf, pulseAlpha * tourOpacity, isDark))
    glow.addColorStop(1, 'rgba(0, 0, 0, 0)')

    ctx.save()
    ctx.globalAlpha = 1
    ctx.fillStyle = glow
    ctx.fillRect(spot.screenX - glowRadius, centerY - glowRadius, glowRadius * 2, glowRadius * 2)
    ctx.restore()
  }
}