From 28b7e90af8f3e3a1b8e8483de16108d5c87daf19 Mon Sep 17 00:00:00 2001 From: Schuwi Date: Sat, 7 Mar 2026 23:19:11 +0100 Subject: [PATCH] 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 --- src/render.rs | 83 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/src/render.rs b/src/render.rs index d914296..fbf7e18 100644 --- a/src/render.rs +++ b/src/render.rs @@ -67,15 +67,47 @@ impl HasPosition for CloudVertex { } } -/// Map cloud cover (0–100 %) 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 { +/// 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 (0–100 %) 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 { + 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 = 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); }