diff --git a/Cargo.lock b/Cargo.lock index 381a724..ac21072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -186,6 +192,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + [[package]] name = "clap" version = "4.5.60" @@ -233,8 +249,11 @@ dependencies = [ "anyhow", "bzip2", "chrono", + "chrono-tz", "clap", "font8x8", + "gif", + "image 0.25.9", "plotters", "rayon", "reqwest", @@ -438,6 +457,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -692,7 +721,20 @@ dependencies = [ "color_quant", "jpeg-decoder", "num-traits", - "png", + "png 0.17.16", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", ] [[package]] @@ -790,6 +832,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -817,6 +869,24 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -860,7 +930,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ce181e3f6bf82d6c1dc569103ca7b1bd964c60ba03d7e6cdfbb3e3eb7f7405" dependencies = [ - "image", + "image 0.24.9", "plotters-backend", ] @@ -877,6 +947,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -904,6 +987,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + [[package]] name = "quinn" version = "0.11.9" @@ -1203,6 +1292,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -1573,6 +1668,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 6da5bfb..6f2a91f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ chrono = { version = "0.4", default-features = false, features = ["std", "cloc anyhow = "1" spade = "2" rayon = "1" +image = { version = "0.25", default-features = false, features = ["png"] } +gif = "0.14" +chrono-tz = "0.10.4" diff --git a/src/main.rs b/src/main.rs index aee3083..c926bfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -139,14 +139,15 @@ fn main() -> Result<()> { } let forecast_dt = dt + chrono::Duration::hours(step as i64); + let forecast_dt = forecast_dt.with_timezone(&chrono_tz::Europe::Berlin); let title = if step == 0 { format!( - "Conditions at {} UTC", + "Conditions at {} CET", forecast_dt.format("%Y-%m-%d %H:%M") ) } else { format!( - "Prediction at {} UTC (+{:02}h)", + "Prediction at {} CET (+{:02}h)", forecast_dt.format("%Y-%m-%d %H:%M"), step ) @@ -167,6 +168,47 @@ fn main() -> Result<()> { eprintln!(" {}", p.display()); } + if output_paths.len() > 1 { + eprintln!("Building animation..."); + let gif_path = cache_dir.join("clct_animation.gif"); + create_gif(&output_paths, &gif_path).context("Creating GIF")?; + eprintln!(" GIF: {}", gif_path.display()); + } + + Ok(()) +} + +fn create_gif(png_paths: &[PathBuf], out_path: &PathBuf) -> Result<()> { + use gif::{Encoder, Frame, Repeat}; + + // Load PNGs and quantize to 256-colour palettes in parallel (the bottleneck). + let n = png_paths.len(); + let frames: Vec> = png_paths + .par_iter() + .enumerate() + .map(|(i, path)| -> Result> { + let img = image::open(path) + .with_context(|| format!("Loading PNG {}", path.display()))? + .into_rgba8(); + let (width, height) = img.dimensions(); + let mut pixels = img.into_raw(); + // speed 8: slightly better quality than the default (10) + let mut frame = + Frame::from_rgba_speed(width as u16, height as u16, &mut pixels, 8); + // Pause longer on first and last frame; 1 unit = 10 ms + frame.delay = if i == 0 || i == n - 1 { 200 } else { 40 }; + Ok(frame) + }) + .collect::>>()?; + + // Write frames sequentially (GIF encoder is not thread-safe). + let file = std::fs::File::create(out_path)?; + let (w, h) = (frames[0].width, frames[0].height); + let mut encoder = Encoder::new(file, w, h, &[])?; + encoder.set_repeat(Repeat::Infinite)?; + for frame in &frames { + encoder.write_frame(frame)?; + } Ok(()) }