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:
@@ -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<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 (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<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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user