diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx index 79cef242..679c457b 100644 --- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx @@ -7,7 +7,7 @@ import { useTheme } from '@/contexts/ThemeContext' import { useVisualDebugSafe } from '@/contexts/VisualDebugContext' import type { ContinentId } from '../continents' import { usePulsingAnimation } from '../features/animations' -import { CustomCursor } from '../features/cursor' +import { CustomCursor, HeatCrosshair } from '../features/cursor' import { useInteractionStateMachine } from '../features/interaction' import { getRenderedViewport, LabelLayer, useD3ForceLabels } from '../features/labels' import { @@ -3391,65 +3391,12 @@ export function MapRenderer({ transform: 'translate(-50%, -50%)', }} > - {/* Compass-style crosshair with heat effects - ring rotates, N stays fixed */} - `rotate(${a}deg)`), - }} - > - {/* Outer ring */} - - {/* Compass tick marks - 12 ticks around the ring */} - {[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330].map((angle) => { - const isCardinal = angle % 90 === 0 - const rad = (angle * Math.PI) / 180 - const innerR = isCardinal ? 10 : 14 - const outerR = 16 - return ( - - ) - })} - {/* Center dot */} - - {/* Counter-rotating group to keep N fixed pointing up */} - `rotate(${-a}deg)`), - }} - > - {/* North indicator - red triangle pointing up */} - - - + )} diff --git a/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx b/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx index ff93d687..f6f798b3 100644 --- a/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx @@ -7,9 +7,10 @@ 'use client' -import { animated, type SpringValue } from '@react-spring/web' +import type { SpringValue } from '@react-spring/web' import { memo } from 'react' import type { HeatCrosshairStyle } from '../../utils/heatStyles' +import { HeatCrosshair } from './HeatCrosshair' // ============================================================================ // Types @@ -42,12 +43,6 @@ export interface CustomCursorProps { flagEmoji?: string | null } -// ============================================================================ -// Constants -// ============================================================================ - -const COMPASS_ANGLES = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330] - // ============================================================================ // Component // ============================================================================ @@ -99,59 +94,7 @@ export const CustomCursor = memo(function CustomCursor({ transition: 'transform 0.1s ease-out', }} > - {/* Compass-style crosshair with heat effects - ring rotates, N stays fixed */} - `rotate(${a}deg)`), - }} - > - {/* Outer ring */} - - {/* Compass tick marks - 12 ticks around the ring */} - {COMPASS_ANGLES.map((angle) => { - const isCardinal = angle % 90 === 0 - const rad = (angle * Math.PI) / 180 - const innerR = isCardinal ? 9 : 11 - const outerR = 13 - return ( - - ) - })} - {/* Center dot */} - - {/* Counter-rotating group to keep N fixed pointing up */} - `rotate(${-a}deg)`), - }} - > - {/* North indicator - red triangle pointing up */} - - - + {/* Cursor region name label - shows what to find under the cursor */} diff --git a/apps/web/src/arcade-games/know-your-world/features/cursor/HeatCrosshair.tsx b/apps/web/src/arcade-games/know-your-world/features/cursor/HeatCrosshair.tsx new file mode 100644 index 00000000..fb535446 --- /dev/null +++ b/apps/web/src/arcade-games/know-your-world/features/cursor/HeatCrosshair.tsx @@ -0,0 +1,144 @@ +/** + * Heat Crosshair Component + * + * Reusable compass-style crosshair SVG with heat-based styling. + * Features rotating outer ring with fixed north indicator. + */ + +'use client' + +import { animated, type SpringValue } from '@react-spring/web' +import { memo } from 'react' +import type { HeatCrosshairStyle } from '../../utils/heatStyles' + +// ============================================================================ +// Constants +// ============================================================================ + +const COMPASS_ANGLES = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330] + +// ============================================================================ +// Types +// ============================================================================ + +export interface HeatCrosshairProps { + /** Size of the crosshair in pixels */ + size: number + /** Rotation angle spring value (degrees) */ + rotationAngle: SpringValue + /** Heat-based styling for crosshair */ + heatStyle: HeatCrosshairStyle + /** Drop shadow intensity (0-1), default 0.5 */ + shadowIntensity?: number +} + +// ============================================================================ +// Component +// ============================================================================ + +/** + * Compass-style crosshair SVG with heat-based styling. + * + * Renders: + * - Outer ring that rotates based on heat level + * - 12 compass tick marks (cardinal directions highlighted in white) + * - Center dot + * - North indicator (red triangle) that counter-rotates to stay pointing up + * + * All dimensions are calculated proportionally based on the `size` prop. + * + * @example + * ```tsx + * + * ``` + */ +export const HeatCrosshair = memo(function HeatCrosshair({ + size, + rotationAngle, + heatStyle, + shadowIntensity = 0.5, +}: HeatCrosshairProps) { + // Calculate proportional dimensions based on size + const center = size / 2 + const ringRadius = size * 0.40625 // 13/32 or 16/40 + const cardinalInnerR = ringRadius * 0.69 // 9/13 or 10/16 + const minorInnerR = ringRadius * 0.846 // 11/13 or 14/16 + const centerDotRadius = size * 0.047 // 1.5/32 or ~1.9/40 + const northTriangleSize = size * 0.125 // 4/32 or 5/40 + + // Cardinal tick stroke width scales with size + const cardinalStrokeWidth = size > 36 ? 2.5 : 2 + const shadowBlur = size > 36 ? 3 : 2 + + return ( + `rotate(${a}deg)`), + }} + > + {/* Outer ring */} + + + {/* Compass tick marks - 12 ticks around the ring */} + {COMPASS_ANGLES.map((angle) => { + const isCardinal = angle % 90 === 0 + const rad = (angle * Math.PI) / 180 + const innerR = isCardinal ? cardinalInnerR : minorInnerR + const outerR = ringRadius + return ( + + ) + })} + + {/* Center dot */} + + + {/* Counter-rotating group to keep N fixed pointing up */} + `rotate(${-a}deg)`), + }} + > + {/* North indicator - red triangle pointing up */} + + + + ) +}) diff --git a/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts b/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts index 852dbe08..f8f34cb7 100644 --- a/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts +++ b/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts @@ -10,3 +10,6 @@ export type { CustomCursorProps, } from './CustomCursor' export { CustomCursor } from './CustomCursor' + +export type { HeatCrosshairProps } from './HeatCrosshair' +export { HeatCrosshair } from './HeatCrosshair'