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 | /** * 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<number> /** 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 * <HeatCrosshair * size={32} * rotationAngle={rotationAngleSpring} * heatStyle={crosshairHeatStyle} * /> * ``` */ 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 ( <animated.svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ filter: `drop-shadow(0 1px ${shadowBlur}px rgba(0,0,0,${shadowIntensity + 0.1}))`, transform: rotationAngle.to((a) => `rotate(${a}deg)`), }} > {/* Outer ring */} <circle cx={center} cy={center} r={ringRadius} fill="none" stroke={heatStyle.color} strokeWidth={heatStyle.strokeWidth} opacity={heatStyle.opacity} /> {/* 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 ( <line key={angle} x1={center + innerR * Math.sin(rad)} y1={center - innerR * Math.cos(rad)} x2={center + outerR * Math.sin(rad)} y2={center - outerR * Math.cos(rad)} stroke={isCardinal ? 'white' : heatStyle.color} strokeWidth={isCardinal ? cardinalStrokeWidth : 1} strokeLinecap="round" opacity={heatStyle.opacity} /> ) })} {/* Center dot */} <circle cx={center} cy={center} r={centerDotRadius} fill={heatStyle.color} opacity={heatStyle.opacity} /> {/* Counter-rotating group to keep N fixed pointing up */} <animated.g style={{ transformOrigin: `${center}px ${center}px`, transform: rotationAngle.to((a) => `rotate(${-a}deg)`), }} > {/* North indicator - red triangle pointing up */} <polygon points={`${center},${center - ringRadius - northTriangleSize * 0.25} ${center - northTriangleSize * 0.5},${center - ringRadius + northTriangleSize * 0.75} ${center + northTriangleSize * 0.5},${center - ringRadius + northTriangleSize * 0.75}`} fill="#ef4444" opacity={0.9} /> </animated.g> </animated.svg> ) }) |