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(())
}