/** * RSRP → color mapping with smooth gradient interpolation. * * CloudRF-style Red → Blue palette: * -130 dBm = dark red (no service) * -100 dBm = yellow (fair) * -70 dBm = green (good) * -50 dBm = deep blue (excellent) * * All functions are pure and allocation-free on the hot path * (pre-built lookup table for fast per-pixel color resolution). */ interface GradientStop { value: number; // normalized 0–1 r: number; g: number; b: number; } const GRADIENT_STOPS: GradientStop[] = [ { value: 0.0, r: 127, g: 0, b: 0 }, // #7f0000 — dark red (no service) { value: 0.15, r: 239, g: 68, b: 68 }, // #EF4444 — red (very weak) { value: 0.30, r: 249, g: 115, b: 22 }, // #F97316 — orange (weak) { value: 0.50, r: 234, g: 179, b: 8 }, // #EAB308 — yellow (fair) { value: 0.70, r: 34, g: 197, b: 94 }, // #22C55E — green (good) { value: 0.85, r: 59, g: 130, b: 246 }, // #3B82F6 — blue (strong) { value: 1.0, r: 37, g: 99, b: 235 }, // #2563EB — deep blue (excellent) ]; /** * Pre-built 256-entry lookup table for O(1) color resolution. * Each entry is [r, g, b]. */ const COLOR_LUT: Uint8Array = buildColorLUT(); function buildColorLUT(): Uint8Array { const size = 256; const lut = new Uint8Array(size * 3); for (let i = 0; i < size; i++) { const value = i / (size - 1); // 0–1 const [r, g, b] = interpolateGradient(value); lut[i * 3] = r; lut[i * 3 + 1] = g; lut[i * 3 + 2] = b; } return lut; } function interpolateGradient(value: number): [number, number, number] { // Find surrounding stops let lower = GRADIENT_STOPS[0]; let upper = GRADIENT_STOPS[GRADIENT_STOPS.length - 1]; for (let i = 0; i < GRADIENT_STOPS.length - 1; i++) { if (value >= GRADIENT_STOPS[i].value && value <= GRADIENT_STOPS[i + 1].value) { lower = GRADIENT_STOPS[i]; upper = GRADIENT_STOPS[i + 1]; break; } } const range = upper.value - lower.value; const t = range > 0 ? (value - lower.value) / range : 0; return [ Math.round(lower.r + (upper.r - lower.r) * t), Math.round(lower.g + (upper.g - lower.g) * t), Math.round(lower.b + (upper.b - lower.b) * t), ]; } /** * Normalize RSRP (dBm) to 0–1 range. * * -130 dBm → 0.0 * -50 dBm → 1.0 */ export function normalizeRSRP(rsrp: number): number { const minRSRP = -130; const maxRSRP = -50; const normalized = (rsrp - minRSRP) / (maxRSRP - minRSRP); return Math.max(0, Math.min(1, normalized)); } /** * Fast color lookup from normalized value (0–1). * Uses pre-built LUT — O(1), no allocations. * * Returns [r, g, b] (0–255 each). */ export function valueToColorRGB(value: number): [number, number, number] { const idx = Math.max(0, Math.min(255, Math.round(value * 255))) * 3; return [COLOR_LUT[idx], COLOR_LUT[idx + 1], COLOR_LUT[idx + 2]]; } /** * Full gradient interpolation (slower, for non-hot-path use like legends). */ export function valueToColor(value: number): [number, number, number] { return interpolateGradient(Math.max(0, Math.min(1, value))); } /** * Gaussian falloff for smooth point edges. * * Returns 0–1 weight based on distance from point center. * sigma defaults to radius/3 for a natural falloff. */ export function gaussianWeight(distance: number, radius: number, sigma?: number): number { const s = sigma ?? radius / 3; return Math.exp(-(distance * distance) / (2 * s * s)); }