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:
Schuwi
2026-03-06 22:53:49 +01:00
parent 0c73e04959
commit 764f4f6378
3 changed files with 150 additions and 4 deletions

View File

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