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 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | 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()
}
}
|