Add animated GIF output with parallelised frame quantization
Render an animated clct_animation.gif alongside the per-step PNGs. Frames are loaded and colour-quantized in parallel via rayon, then written sequentially with the gif crate. Also converts timestamps to CET/CEST using chrono-tz. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
105
Cargo.lock
generated
105
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
46
src/main.rs
46
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<Frame<'static>> = png_paths
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(|(i, path)| -> Result<Frame<'static>> {
|
||||
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::<Result<Vec<_>>>()?;
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user