116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
/**
|
||
* 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));
|
||
}
|