diff --git a/Cargo.lock b/Cargo.lock
index f84b2001dfd3f5b2448e6eb781b42fd87b4552fe..be444f847c8058f489ac93a00620ecbdec19fbc8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -787,10 +787,12 @@ dependencies = [
 name = "rapidriter-cat"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "bitvec",
  "clap",
  "colored",
  "console",
+ "gif",
  "image",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 81bc4a9d0e940ac6c2719edeeec4655e2891a352..f8b97156e38461206c42a3d947c8c2a26cf03980 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,10 +7,12 @@ edition = "2021"
 crate-type = ["cdylib", "rlib"]
 
 [dependencies]
+anyhow = "1.0.96"
 bitvec = "1.0.1"
 clap = { version = "4.5.30", features = ["derive"] }
 colored = "3.0.0"
 console = "0.15.10"
+gif = "0.13.1"
 
 [build-dependencies]
 image = "0.25.5"
diff --git a/src/main.rs b/src/main.rs
index b9174f572b324199d2b1f11d2ed031d99a108c3a..af1e0794d08f1bfafead489d4736ff835c83b6b0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,7 @@
 use std::{
-    io::{self, Read},
+    borrow::Cow,
+    fs::File,
+    path::PathBuf,
     thread,
     time::{Duration, Instant},
 };
@@ -9,6 +11,7 @@ use bitvec::{order::Msb0, view::AsBits};
 use clap::Parser;
 use colored::Colorize;
 use console::Term;
+use gif::{Encoder, Frame, Repeat};
 use rapidriter_cat::next_frame;
 
 const WIDTH: usize = 96;
@@ -16,17 +19,59 @@ const HEIGHT: usize = 38;
 
 mod arg;
 
-fn main() {
+fn main() -> anyhow::Result<()> {
     let args = Args::parse();
 
-    // clear screen
-    print!("\x1B[2J");
-
-    if args.interactive {
-        interactive_mode();
+    if let Some(path) = args.out {
+        generate_gif(path)?;
     } else {
-        non_interactive_mode();
+        // clear screen
+        print!("\x1B[2J");
+
+        if args.interactive {
+            interactive_mode();
+        } else {
+            non_interactive_mode();
+        }
     }
+
+    Ok(())
+}
+
+fn generate_gif(path: PathBuf) -> anyhow::Result<()> {
+    let mut gif = File::create(path)?;
+    let mut encoder = Encoder::new(
+        &mut gif,
+        WIDTH.try_into().unwrap(),
+        HEIGHT.try_into().unwrap(),
+        &[0, 0, 0, 0xFF, 0, 0],
+    )?;
+    encoder.set_repeat(Repeat::Infinite)?;
+
+    let mut frame = [0; 456];
+    for i in 0..100 {
+        let done = unsafe { next_frame(i, frame.as_mut_ptr()) } == 1;
+        let mut exploded_frame = vec![];
+        for b in frame.as_bits::<Msb0>() {
+            exploded_frame.push(if *b { 1 } else { 0 });
+        }
+
+        let f = Frame {
+            delay: 10, // 100 ms, or 10 FPS
+            width: WIDTH.try_into().unwrap(),
+            height: HEIGHT.try_into().unwrap(),
+            buffer: Cow::Borrowed(&*exploded_frame),
+
+            ..Default::default()
+        };
+        encoder.write_frame(&f)?;
+
+        if done {
+            break;
+        }
+    }
+
+    Ok(())
 }
 
 fn interactive_mode() {
@@ -74,12 +119,14 @@ fn non_interactive_mode() {
             thread::sleep(target - now);
         }
 
-        if unsafe { next_frame(i, frame.as_mut_ptr()) } == 1 {
-            break;
-        }
+        let done = unsafe { next_frame(i, frame.as_mut_ptr()) } == 1;
 
         draw_frame(&frame);
         println!("Frame: {}", i);
+
+        if done {
+            break;
+        }
     }
 }