Improve rendering: barycentric interpolation, Gaussian blur, white-cloud palette
Replace nearest-neighbour BFS flood fill with Delaunay triangulation (spade) + barycentric interpolation for smooth cloud boundaries. Add NaN-aware separable Gaussian blur (σ=8 px, rayon-parallelised) to remove triangulation facets. Switch colour scheme from blue bands to a continuous green-to-white opacity blend matching the DWD app style. Pixel interpolation loop is also parallelised with rayon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
100
Cargo.lock
generated
100
Cargo.lock
generated
@@ -8,6 +8,12 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -230,7 +236,9 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"font8x8",
|
"font8x8",
|
||||||
"plotters",
|
"plotters",
|
||||||
|
"rayon",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"spade",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -260,6 +268,31 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -271,6 +304,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -296,6 +341,12 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "font8x8"
|
name = "font8x8"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -387,6 +438,17 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -941,6 +1003,26 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.28"
|
version = "0.12.28"
|
||||||
@@ -995,6 +1077,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robust"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -1137,6 +1225,18 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spade"
|
||||||
|
version = "2.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
"num-traits",
|
||||||
|
"robust",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ font8x8 = "0.2"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
spade = "2"
|
||||||
|
rayon = "1"
|
||||||
|
|||||||
220
src/render.rs
220
src/render.rs
@@ -2,7 +2,8 @@ use crate::projection::OrthoProjection;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use font8x8::{UnicodeFonts, BASIC_FONTS};
|
use font8x8::{UnicodeFonts, BASIC_FONTS};
|
||||||
use plotters::prelude::*;
|
use plotters::prelude::*;
|
||||||
use std::collections::VecDeque;
|
use rayon::prelude::*;
|
||||||
|
use spade::{DelaunayTriangulation, HasPosition, Point2, Triangulation};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
const IMG_WIDTH: u32 = 900;
|
const IMG_WIDTH: u32 = 900;
|
||||||
@@ -14,20 +15,8 @@ const CENTER_LON: f64 = 13.08;
|
|||||||
const HALF_W_DEG: f64 = 0.8; // horizontal half-extent in degrees of arc
|
const HALF_W_DEG: f64 = 0.8; // horizontal half-extent in degrees of arc
|
||||||
const HALF_H_DEG: f64 = 0.4; // vertical half-extent
|
const HALF_H_DEG: f64 = 0.4; // vertical half-extent
|
||||||
|
|
||||||
/// Cloud-cover thresholds and their associated RGB colours.
|
|
||||||
/// Values below the first threshold are drawn as the background colour.
|
|
||||||
const LEVELS: [(f32, (u8, u8, u8)); 7] = [
|
|
||||||
(1.0, (208, 232, 248)), // very thin
|
|
||||||
(2.0, (160, 200, 232)),
|
|
||||||
(5.0, (112, 168, 216)),
|
|
||||||
(10.0, ( 64, 128, 192)),
|
|
||||||
(20.0, ( 32, 80, 160)),
|
|
||||||
(50.0, ( 16, 32, 96)),
|
|
||||||
(100.0, ( 5, 10, 48)),
|
|
||||||
];
|
|
||||||
|
|
||||||
const BACKGROUND: RGBColor = RGBColor(240, 248, 255); // pale sky-blue
|
const BACKGROUND: RGBColor = RGBColor(240, 248, 255); // pale sky-blue
|
||||||
const LAND_BG: RGBColor = RGBColor(232, 236, 220); // muted green for land
|
const LAND_BG: RGBColor = RGBColor(142, 178, 35); // strong green for land
|
||||||
const BLACK: RGBColor = RGBColor( 0, 0, 0);
|
const BLACK: RGBColor = RGBColor( 0, 0, 0);
|
||||||
const RED: RGBColor = RGBColor(220, 30, 30);
|
const RED: RGBColor = RGBColor(220, 30, 30);
|
||||||
|
|
||||||
@@ -42,16 +31,28 @@ const LOCATIONS: &[(&str, f64, f64)] = &[
|
|||||||
("Berlin (Mitte)", 13.39, 52.52),
|
("Berlin (Mitte)", 13.39, 52.52),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct CloudVertex {
|
||||||
|
position: Point2<f64>,
|
||||||
|
cloud: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasPosition for CloudVertex {
|
||||||
|
type Scalar = f64;
|
||||||
|
fn position(&self) -> Point2<f64> {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
fn cloud_color(cover: f32) -> Option<RGBColor> {
|
||||||
if cover < 1.0 {
|
if cover < 1.0 {
|
||||||
return None; // clear sky — use background
|
return None;
|
||||||
}
|
}
|
||||||
for &(threshold, (r, g, b)) in LEVELS.iter().rev() {
|
let t = (cover / 100.0).clamp(0.0, 1.0);
|
||||||
if cover >= threshold {
|
let blend = |base: u8| -> u8 { (base as f32 + t * (255.0 - base as f32)).round() as u8 };
|
||||||
return Some(RGBColor(r, g, b));
|
Some(RGBColor(blend(LAND_BG.0), blend(LAND_BG.1), blend(LAND_BG.2)))
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(RGBColor(LEVELS[0].1 .0, LEVELS[0].1 .1, LEVELS[0].1 .2))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw ASCII text using the built-in 8×8 pixel font.
|
/// Draw ASCII text using the built-in 8×8 pixel font.
|
||||||
@@ -91,6 +92,57 @@ fn draw_pixel_text(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Separable Gaussian blur on a flat f32 grid.
|
||||||
|
///
|
||||||
|
/// NaN pixels are treated as transparent: they contribute zero weight and the
|
||||||
|
/// result is renormalised over non-NaN neighbours only. Pixels with no non-NaN
|
||||||
|
/// neighbours remain NaN.
|
||||||
|
fn gaussian_blur(grid: &[f32], w: usize, h: usize, sigma: f32) -> Vec<f32> {
|
||||||
|
let radius = (3.0 * sigma).ceil() as usize;
|
||||||
|
let kernel: Vec<f32> = (0..=2 * radius)
|
||||||
|
.map(|i| {
|
||||||
|
let x = i as f32 - radius as f32;
|
||||||
|
(-x * x / (2.0 * sigma * sigma)).exp()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Horizontal pass
|
||||||
|
let temp: Vec<f32> = (0..h * w)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|idx| {
|
||||||
|
let row = idx / w;
|
||||||
|
let col = idx % w;
|
||||||
|
let mut val = 0.0f32;
|
||||||
|
let mut wsum = 0.0f32;
|
||||||
|
for (ki, &k) in kernel.iter().enumerate() {
|
||||||
|
let c = col as i64 + ki as i64 - radius as i64;
|
||||||
|
if c < 0 || c >= w as i64 { continue; }
|
||||||
|
let v = grid[row * w + c as usize];
|
||||||
|
if !v.is_nan() { val += k * v; wsum += k; }
|
||||||
|
}
|
||||||
|
if wsum > 0.0 { val / wsum } else { f32::NAN }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Vertical pass
|
||||||
|
(0..h * w)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|idx| {
|
||||||
|
let row = idx / w;
|
||||||
|
let col = idx % w;
|
||||||
|
let mut val = 0.0f32;
|
||||||
|
let mut wsum = 0.0f32;
|
||||||
|
for (ki, &k) in kernel.iter().enumerate() {
|
||||||
|
let r = row as i64 + ki as i64 - radius as i64;
|
||||||
|
if r < 0 || r >= h as i64 { continue; }
|
||||||
|
let v = temp[r as usize * w + col];
|
||||||
|
if !v.is_nan() { val += k * v; wsum += k; }
|
||||||
|
}
|
||||||
|
if wsum > 0.0 { val / wsum } else { f32::NAN }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Render one forecast frame as a PNG.
|
/// Render one forecast frame as a PNG.
|
||||||
///
|
///
|
||||||
/// - `lats`, `lons`, `cloud_cover` must all have the same length.
|
/// - `lats`, `lons`, `cloud_cover` must all have the same length.
|
||||||
@@ -119,54 +171,89 @@ pub fn render_frame(
|
|||||||
|
|
||||||
map_area.fill(&LAND_BG).context("fill map area")?;
|
map_area.fill(&LAND_BG).context("fill map area")?;
|
||||||
|
|
||||||
// Nearest-neighbour fill via multi-source BFS.
|
// Build a Delaunay triangulation in continuous pixel-space coordinates.
|
||||||
//
|
// Include all grid points that project within a small margin outside the image
|
||||||
// Each data point seeds one pixel in a flat grid. BFS then propagates each
|
// bounds so that boundary pixels interpolate cleanly without edge artefacts.
|
||||||
// value outward to all 4-connected NaN neighbours, so every reachable pixel
|
const TRI_MARGIN: f64 = 10.0;
|
||||||
// ends up holding the value of the closest source point (by Manhattan distance).
|
let mut tri = DelaunayTriangulation::<CloudVertex>::new();
|
||||||
// Complexity: O(map_w × map_h) — ~410 k array ops, no hashing.
|
|
||||||
let grid_w = map_w as usize;
|
|
||||||
let grid_h = map_h as usize;
|
|
||||||
let mut grid = vec![f32::NAN; grid_w * grid_h];
|
|
||||||
let mut queue: VecDeque<(i32, i32)> = VecDeque::new();
|
|
||||||
|
|
||||||
// Seed the grid with projected data points.
|
|
||||||
for i in 0..lats.len() {
|
for i in 0..lats.len() {
|
||||||
let cover = cloud_cover[i];
|
let cover = cloud_cover[i];
|
||||||
if cover.is_nan() {
|
if cover.is_nan() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some((px, py)) = proj.project(lats[i] as f64, lons[i] as f64) else { continue };
|
let Some((px, py)) = proj.project(lats[i] as f64, lons[i] as f64) else { continue };
|
||||||
let Some((col, row)) = proj.to_pixel(px, py, map_w, map_h) else { continue };
|
// Continuous pixel coordinates — no rounding.
|
||||||
let idx = row as usize * grid_w + col as usize;
|
let col_f = (px + HALF_W_DEG) / (2.0 * HALF_W_DEG) * map_w as f64;
|
||||||
if grid[idx].is_nan() {
|
let row_f = (-py + HALF_H_DEG) / (2.0 * HALF_H_DEG) * map_h as f64;
|
||||||
grid[idx] = cover;
|
if col_f < -TRI_MARGIN
|
||||||
queue.push_back((col, row));
|
|| col_f > map_w as f64 + TRI_MARGIN
|
||||||
|
|| row_f < -TRI_MARGIN
|
||||||
|
|| row_f > map_h as f64 + TRI_MARGIN
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
let _ = tri.insert(CloudVertex { position: Point2::new(col_f, row_f), cloud: cover });
|
||||||
}
|
}
|
||||||
|
|
||||||
// BFS flood fill.
|
// For each output pixel, locate the containing triangle and barycentric-interpolate
|
||||||
const DIRS: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
|
// the cloud cover from its three vertices. The triangulation is immutable at this
|
||||||
while let Some((col, row)) = queue.pop_front() {
|
// point, so the work is embarrassingly parallel via rayon.
|
||||||
let cover = grid[row as usize * grid_w + col as usize];
|
let grid_w = map_w as usize;
|
||||||
for (dc, dr) in DIRS {
|
let grid_h = map_h as usize;
|
||||||
let nc = col + dc;
|
let cover_grid: Vec<f32> = (0..grid_h * grid_w)
|
||||||
let nr = row + dr;
|
.into_par_iter()
|
||||||
if nc < 0 || nc >= map_w as i32 || nr < 0 || nr >= map_h as i32 {
|
.map(|i| {
|
||||||
continue;
|
let col = (i % grid_w) as f64 + 0.5;
|
||||||
|
let row = (i / grid_w) as f64 + 0.5;
|
||||||
|
let p = Point2::new(col, row);
|
||||||
|
use spade::PositionInTriangulation::*;
|
||||||
|
match tri.locate(p) {
|
||||||
|
OnVertex(v) => tri.vertex(v).data().cloud,
|
||||||
|
OnEdge(e) => {
|
||||||
|
// Linear interpolation along the edge.
|
||||||
|
let edge = tri.directed_edge(e);
|
||||||
|
let a = edge.from();
|
||||||
|
let b = edge.to();
|
||||||
|
let (ax, ay) = (a.position().x, a.position().y);
|
||||||
|
let (bx, by) = (b.position().x, b.position().y);
|
||||||
|
let ab2 = (bx - ax).powi(2) + (by - ay).powi(2);
|
||||||
|
let t = if ab2 == 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(((p.x - ax) * (bx - ax) + (p.y - ay) * (by - ay)) / ab2)
|
||||||
|
.clamp(0.0, 1.0)
|
||||||
|
};
|
||||||
|
a.data().cloud * (1.0 - t) as f32 + b.data().cloud * t as f32
|
||||||
|
}
|
||||||
|
OnFace(f) => {
|
||||||
|
let face = tri.face(f);
|
||||||
|
let [v0, v1, v2] = face.vertices();
|
||||||
|
let (ax, ay) = (v0.position().x, v0.position().y);
|
||||||
|
let (bx, by) = (v1.position().x, v1.position().y);
|
||||||
|
let (cx, cy) = (v2.position().x, v2.position().y);
|
||||||
|
let denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy);
|
||||||
|
if denom == 0.0 {
|
||||||
|
return v0.data().cloud;
|
||||||
|
}
|
||||||
|
let l0 = ((by - cy) * (p.x - cx) + (cx - bx) * (p.y - cy)) / denom;
|
||||||
|
let l1 = ((cy - ay) * (p.x - cx) + (ax - cx) * (p.y - cy)) / denom;
|
||||||
|
let l2 = 1.0 - l0 - l1;
|
||||||
|
v0.data().cloud * l0 as f32
|
||||||
|
+ v1.data().cloud * l1 as f32
|
||||||
|
+ v2.data().cloud * l2 as f32
|
||||||
|
}
|
||||||
|
OutsideOfConvexHull(_) | NoTriangulation => f32::NAN,
|
||||||
}
|
}
|
||||||
let nidx = nr as usize * grid_w + nc as usize;
|
})
|
||||||
if grid[nidx].is_nan() {
|
.collect();
|
||||||
grid[nidx] = cover;
|
|
||||||
queue.push_back((nc, nr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint the filled grid.
|
const BLUR_SIGMA: f32 = 8.0;
|
||||||
|
let cover_grid = gaussian_blur(&cover_grid, grid_w, grid_h, BLUR_SIGMA);
|
||||||
|
|
||||||
|
// Paint the interpolated grid.
|
||||||
for row in 0..map_h as i32 {
|
for row in 0..map_h as i32 {
|
||||||
for col in 0..map_w as i32 {
|
for col in 0..map_w as i32 {
|
||||||
let cover = grid[row as usize * grid_w + col as usize];
|
let cover = cover_grid[row as usize * grid_w + col as usize];
|
||||||
if let Some(color) = cloud_color(cover) {
|
if let Some(color) = cloud_color(cover) {
|
||||||
map_area.draw_pixel((col, row), &color).ok();
|
map_area.draw_pixel((col, row), &color).ok();
|
||||||
}
|
}
|
||||||
@@ -231,22 +318,17 @@ fn draw_legend(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// "< 1%" row (clear sky)
|
|
||||||
draw_entry("< 1%", LAND_BG, y as i32 + 22);
|
|
||||||
|
|
||||||
// Remaining threshold rows — use ASCII hyphen instead of en-dash
|
|
||||||
let entries = [
|
let entries = [
|
||||||
(1.0f32, "1-2%"),
|
(0.0f32, "< 1%"),
|
||||||
(2.0, "2-5%"),
|
(25.0, "~25%"),
|
||||||
(5.0, "5-10%"),
|
(50.0, "~50%"),
|
||||||
(10.0, "10-20%"),
|
(75.0, "~75%"),
|
||||||
(20.0, "20-50%"),
|
(100.0, "100%"),
|
||||||
(50.0, "50-100%"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (i, &(lo, label)) in entries.iter().enumerate() {
|
for (i, &(cover, label)) in entries.iter().enumerate() {
|
||||||
let row_y = y as i32 + 22 + (i as i32 + 1) * (box_h + 2);
|
let row_y = y as i32 + 22 + i as i32 * (box_h + 2);
|
||||||
let color = cloud_color(lo).unwrap_or(BACKGROUND);
|
let color = cloud_color(cover).unwrap_or(LAND_BG);
|
||||||
draw_entry(label, color, row_y);
|
draw_entry(label, color, row_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user