Files
brahman/crates/modules/pineal/heatmap/src/palette.rs
T
sergio 4528e08e04 feat(pineal): cierra stub heatmap — matrix + viridis + encoder + paint
Fase F: segundo stub de pineal cerrado.

- matrix — HeatmapMatrix densa width×height de f32, con revision para
  invalidación de textura; get/set/min_max/replace_data.
- palette — Ramp::{Viridis, Grayscale}; Viridis por interpolación
  lineal de 5 control points perceptualmente uniformes.
- encoder — encode_argb: normaliza por min/max + rampa + pack 0xAARRGGBB
  para subir como textura (camino de matrices grandes).
- paint — painter agnóstico: un fill_rect por celda contra un Canvas
  (camino de matrices chicas + export SVG).

12 tests verdes. cargo check --workspace verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:13:10 +00:00

95 lines
2.6 KiB
Rust

//! Color ramps para heatmaps. Interpolación lineal entre control points.
use pineal_render::Color;
/// Rampa de color. `sample(t)` mapea `t ∈ [0,1]` a un `Color`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ramp {
/// Viridis — perceptualmente uniforme, dark-purple → yellow.
Viridis,
/// Escala de grises lineal, negro → blanco.
Grayscale,
}
impl Ramp {
/// Mapea `t` (se clampa a `[0,1]`) a un color de la rampa.
pub fn sample(&self, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
match self {
Ramp::Grayscale => Color::rgb(t, t, t),
Ramp::Viridis => lerp_stops(t, VIRIDIS),
}
}
}
/// Control points de Viridis (aproximación de 5 stops del colormap real).
const VIRIDIS: &[(f32, u32)] = &[
(0.00, 0x440154),
(0.25, 0x3b528b),
(0.50, 0x21918c),
(0.75, 0x5ec962),
(1.00, 0xfde725),
];
/// Interpola linealmente entre los stops `(pos, hex)` ordenados por `pos`.
fn lerp_stops(t: f32, stops: &[(f32, u32)]) -> Color {
if stops.is_empty() {
return Color::BLACK;
}
if t <= stops[0].0 {
return Color::from_hex(stops[0].1);
}
let last = stops[stops.len() - 1];
if t >= last.0 {
return Color::from_hex(last.1);
}
for w in stops.windows(2) {
let (p0, c0) = w[0];
let (p1, c1) = w[1];
if t >= p0 && t <= p1 {
let local = (t - p0) / (p1 - p0);
return lerp_color(Color::from_hex(c0), Color::from_hex(c1), local);
}
}
Color::from_hex(last.1)
}
fn lerp_color(a: Color, b: Color, t: f32) -> Color {
Color::rgba(
a.r + (b.r - a.r) * t,
a.g + (b.g - a.g) * t,
a.b + (b.b - a.b) * t,
a.a + (b.a - a.a) * t,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grayscale_endpoints() {
assert_eq!(Ramp::Grayscale.sample(0.0), Color::BLACK);
assert_eq!(Ramp::Grayscale.sample(1.0), Color::WHITE);
}
#[test]
fn viridis_endpoints_match_control_points() {
assert_eq!(Ramp::Viridis.sample(0.0), Color::from_hex(0x440154));
assert_eq!(Ramp::Viridis.sample(1.0), Color::from_hex(0xfde725));
}
#[test]
fn sample_clamps_out_of_range() {
assert_eq!(Ramp::Viridis.sample(-5.0), Ramp::Viridis.sample(0.0));
assert_eq!(Ramp::Viridis.sample(5.0), Ramp::Viridis.sample(1.0));
}
#[test]
fn viridis_midpoint_is_between() {
let mid = Ramp::Viridis.sample(0.5);
// El stop de 0.5 es 0x21918c.
assert_eq!(mid, Color::from_hex(0x21918c));
}
}