Files
rfcp/frontend/src/utils/colorGradient.ts
2026-02-02 21:30:00 +02:00

116 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 01
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); // 01
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 01 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 (01).
* Uses pre-built LUT — O(1), no allocations.
*
* Returns [r, g, b] (0255 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 01 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));
}