Improve color scheme

- boost base map contrast
- adjust cloud colors to a slightly bluish shade
- start drawing lowest cloud cover percentage above 0 at 30% alpha
This commit is contained in:
Schuwi
2026-03-07 23:19:11 +01:00
parent c7c08ebead
commit 28b7e90af8

View File

@@ -67,15 +67,47 @@ impl HasPosition for CloudVertex {
}
}
/// Map cloud cover (0100 %) to a colour by blending the land background toward
/// white. Returns `None` for near-zero cover so the land background shows through.
fn cloud_color(cover: f32) -> Option<RGBColor> {
/// Target colour for 100 % cloud cover: a light blue-white, like an overcast sky.
/// Using a tinted target (rather than pure white) gives better contrast against
/// the pale yellow-beige tones of the basemap.
const CLOUD_TARGET: [f32; 3] = [226.0, 230.0, 239.0];
/// Boost the perceptual saturation of an RGB pixel by `factor`.
/// Uses a luma-weighted approach: moves each channel away from the grey value.
fn boost_saturation(pixel: [u8; 3], factor: f32) -> [u8; 3] {
let [r, g, b] = pixel;
let lum = 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32;
let clamp_u8 = |v: f32| v.round().clamp(0.0, 255.0) as u8;
[
clamp_u8(lum + factor * (r as f32 - lum)),
clamp_u8(lum + factor * (g as f32 - lum)),
clamp_u8(lum + factor * (b as f32 - lum)),
]
}
fn scale_cloud_cover(cover: f32) -> f32 {
if cover < 1.0 {
0.0
} else {
30.0 + 0.7 * cover
}
}
/// Map cloud cover (0100 %) to a colour by blending the base color toward
/// `CLOUD_TARGET`. Returns `None` for near-zero cover or NaN so the land background shows through.
fn cloud_color(base_color: RGBColor, cover: f32) -> Option<RGBColor> {
if cover.is_nan() || cover < 1.0 {
return None;
}
let t = (cover / 100.0).clamp(0.0, 1.0);
let blend = |base: u8| -> u8 { (base as f32 + t * (255.0 - base as f32)).round() as u8 };
Some(RGBColor(blend(LAND_BG.0), blend(LAND_BG.1), blend(LAND_BG.2)))
let blend = |base: u8, target: f32| -> u8 {
(base as f32 + t * (target - base as f32)).round() as u8
};
Some(RGBColor(
blend(base_color.0, CLOUD_TARGET[0]),
blend(base_color.1, CLOUD_TARGET[1]),
blend(base_color.2, CLOUD_TARGET[2]),
))
}
/// Draw ASCII text using the built-in 8×8 pixel font.
@@ -215,7 +247,7 @@ pub fn render_frame(
{
continue;
}
let _ = tri.insert(CloudVertex { position: Point2::new(col_f, row_f), cloud: cover });
let _ = tri.insert(CloudVertex { position: Point2::new(col_f, row_f), cloud: scale_cloud_cover(cover) });
}
// For each output pixel, locate the containing triangle and barycentric-interpolate
@@ -273,22 +305,19 @@ pub fn render_frame(
const BLUR_SIGMA: f32 = 8.0;
let cover_grid = gaussian_blur(&cover_grid, grid_w, grid_h, BLUR_SIGMA);
// Build pixel buffer: start from basemap or flat LAND_BG.
let mut pixel_buf: Vec<[u8; 3]> = if let Some(bm) = basemap {
bm.to_vec()
// Build pixel buffer: start from basemap (saturation-boosted) or flat LAND_BG.
const SAT_BOOST: f32 = 1.8;
let mut pixel_buf: Vec<RGBColor> = if let Some(bm) = basemap {
bm.iter().map(|&p| boost_saturation(p, SAT_BOOST)).map(|p| RGBColor(p[0], p[1], p[2])).collect()
} else {
vec![[LAND_BG.0, LAND_BG.1, LAND_BG.2]; n_pixels]
vec![LAND_BG; n_pixels]
};
// Blend cloud cover toward white.
// Blend cloud cover toward CLOUD_TARGET.
for (idx, &cover) in cover_grid.iter().enumerate() {
if cover.is_nan() || cover < 1.0 {
continue;
if let Some(cloud_col) = cloud_color(pixel_buf[idx], cover) {
pixel_buf[idx] = cloud_col;
}
let t = (cover / 100.0).clamp(0.0, 1.0);
let [r, g, b] = pixel_buf[idx];
let blend = |base: u8| -> u8 { (base as f32 + t * (255.0 - base as f32)).round() as u8 };
pixel_buf[idx] = [blend(r), blend(g), blend(b)];
}
// Composite label overlay (map labels above clouds).
@@ -296,20 +325,20 @@ pub fn render_frame(
for (idx, &[lr, lg, lb, la]) in lbl.iter().enumerate() {
if la == 0 { continue; }
let a = la as f32 / 255.0;
let [r, g, b] = pixel_buf[idx];
pixel_buf[idx] = [
let RGBColor(r, g, b) = pixel_buf[idx];
pixel_buf[idx] = RGBColor(
(lr as f32 * a + r as f32 * (1.0 - a)).round() as u8,
(lg as f32 * a + g as f32 * (1.0 - a)).round() as u8,
(lb as f32 * a + b as f32 * (1.0 - a)).round() as u8,
];
);
}
}
// Flush pixel buffer to drawing area.
for row in 0..map_h as i32 {
for col in 0..map_w as i32 {
let [r, g, b] = pixel_buf[row as usize * map_w as usize + col as usize];
map_area.draw_pixel((col, row), &RGBColor(r, g, b)).ok();
let pixel = pixel_buf[row as usize * map_w as usize + col as usize];
map_area.draw_pixel((col, row), &pixel).ok();
}
}
@@ -370,16 +399,16 @@ fn draw_legend(
};
let entries = [
(0.0f32, "< 1%"),
(25.0, "~25%"),
(50.0, "~50%"),
(75.0, "~75%"),
(0.0f32, "Clear"),
(1.0, "1%"),
(25.0, "25%"),
(50.0, "50%"),
(100.0, "100%"),
];
for (i, &(cover, label)) in entries.iter().enumerate() {
let row_y = y as i32 + 22 + i as i32 * (box_h + 2);
let color = cloud_color(cover).unwrap_or(LAND_BG);
let color = cloud_color(LAND_BG, scale_cloud_cover(cover)).unwrap_or(LAND_BG);
draw_entry(label, color, row_y);
}