惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Project Zero
Project Zero
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
Jina AI
Jina AI
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 叶小钗
GbyAI
GbyAI
阮一峰的网络日志
阮一峰的网络日志
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Hacker News: Ask HN
Hacker News: Ask HN
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Privacy & Cybersecurity Law Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
A
About on SuperTechFans
D
DataBreaches.Net
The Cloudflare Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
大猫的无限游戏
大猫的无限游戏
Know Your Adversary
Know Your Adversary
T
Tenable Blog
N
News and Events Feed by Topic
PCI Perspectives
PCI Perspectives
The Register - Security
The Register - Security
O
OpenAI News
G
Google Developers Blog
T
The Blog of Author Tim Ferriss
C
CERT Recently Published Vulnerability Notes
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Palo Alto Networks Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
N
Netflix TechBlog - Medium
V
Vulnerabilities – Threatpost
月光博客
月光博客
Recorded Future
Recorded Future
P
Proofpoint News Feed
Cisco Talos Blog
Cisco Talos Blog
P
Privacy International News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
博客园 - Franky
Latest news
Latest news
MongoDB | Blog
MongoDB | Blog
V
Visual Studio Blog

Pierce Freeman

A browser for agents | Pierce Freeman The grey market of podcast appearances The way I travel | Pierce Freeman Fixing slow AWS uploads | Pierce Freeman Local tools should still use vaults We solved scratch content first Starting a podcast in 2025 Being late but still being early Automating our home video imports Adding my parents to tailscale A deep dive on agent sandboxes Language servers for AI | Pierce Freeman My simple home podcast studio We need centralized infrastructure | Pierce Freeman Coercing agents to follow conventions using AST validation My unified theory of social selling My personal backup strategy | Pierce Freeman July updates to the homelab How the KV Cache works httpx is the right way to do web requests in Python Reputation is becoming everything | Pierce Freeman Building a (kind of) invisible mac app Updated knowledge in language models How speculative decoding works | Pierce Freeman Under the hood of Claude Code Doing things because they're easy, not hard Speeding up sideeffects with JIT in mountaineer Firehot for hot reloading in Python Misadventures in Python hot reloading How text diffusion works | Pierce Freeman The tenacity of modern LLMs The ergonomics of rails | Pierce Freeman How language servers work | Pierce Freeman Just add eggs | Pierce Freeman Unfortunately SEO still matters | Pierce Freeman The futility of human-only web requirements Setting up Input Leap | Pierce Freeman Checking in on Waymo | Pierce Freeman The react revolution | Pierce Freeman Speeding up many small transfers to a unifi nas Quick notes on swift libraries AI engineering is a different animal San Francisco | Pierce Freeman Debugging a mountaineer rendering segfault Local network config on macOS Building our home network | Pierce Freeman Introducing Envelope.dev | Pierce Freeman Legacy code and AI copilots Typehinting from day-zero | Pierce Freeman Generating database migrations with acyclic graphs Lofoten | Pierce Freeman Mountaineer v0.1: Webapps in Python and React Constraining LLM Outputs | Pierce Freeman Passthrough above all | Pierce Freeman Accuracy in kudos | Pierce Freeman How quick we are to adapt The curious case of LM repetition Costa Rica | Pierce Freeman Debugging chrome extensions with system-level logging Speeding up runpod | Pierce Freeman Inline footnotes with html templates Parsing Common Crawl in a day for $60 An era of rich CLI All or nothing with remote work The Next 10 Years | Pierce Freeman Adding wheels to flash-attention | Pierce Freeman LLMs as interdisciplinary agents | Pierce Freeman New Zealand | Pierce Freeman Representations in autoregressive models | Pierce Freeman Let's talk about Siri | Pierce Freeman Minimum viable public infrastructure | Pierce Freeman Reasoning vs. Memorization in LLMs Automatically migrate enums in alembic Greater sequence lengths will set us free On learning to ski | Pierce Freeman Dolomites | Pierce Freeman Using grpc with node and typescript Opportunity years | Pierce Freeman Buzzword peaks and valleys | Pierce Freeman Buenos Aires | Pierce Freeman Network routing interaction on MacOS Independent work: November recap | Pierce Freeman Debugging slow pytorch training performance The provenance of copy and paste Debugging tips for neural network training Patagonia | Pierce Freeman Santiago | Pierce Freeman My 2022 digital travel kit AWS vs GCP - GPU Availability V2 Independent work: October recap | Pierce Freeman Planning Patagonia | Pierce Freeman Relationship modeling | Pierce Freeman The power of status updates A new chapter | Pierce Freeman Give my library a coffee shop AWS vs GCP - GPU Availability V1 Switzerland | Pierce Freeman Headfull browsers beat headless | Pierce Freeman Webcrawling tradeoffs | Pierce Freeman Copenhagen | Pierce Freeman
Making an ascii animation | Pierce Freeman
2025-07-15 · via Pierce Freeman

Ascii art

If you're in the market for a new terminal app, there's a surprising number of options to choose right now. Ghostty's one way to go - authored as a full open source project by Mitchell Hashimoto.1

Ghostty is a minimalist terminal emulator. So is their website: just an animation with a download button. But this animation really caught my eye. It's running at 24fps with proper aspect ratio correction, color-aware filtering, and accurate luminance mapping. And it's all in ascii. Chic.

Thankfully they open sourced the original video and the conversion script so we can see exactly how they did it.

The video

Screenshot of video

The terminal graphic starts its life as a video file, which is probably no surprise since the ascii animation renders at a high framerate as well. I doubted you'd go through the effort of trying to work up something that sophisticated in a non-animation editor.

Interestingly, the file itself doesn't loop smoothly. The last frame doesn't transition cleanly back to the first frame, so there's a visible "pop" if you just loop the raw video. The website doesn't have this same kind of pop and in fact the checked-in first frame isn't equal to the first frame of the video.

My guess is they're used a cropped version of the video when they actually generated the animation.

Color filtering

Okay, let's look at the conversion script. It's written bash and it's actually pretty short, since it leans on just a few algorithms.

Making "images" in ascii is really just choosing the text that you want in each spot. Negative space (no text) becomes just as important as the positive space where you put text itself. Rather than converting every pixel to ASCII, it uses a distance-based filter to only render pixels that are close to two target colors:

BLUE="0,0,230"
BLUE_DISTANCE_TOLERANCE=90

WHITE="215,215,215"
WHITE_DISTANCE_TOLERANCE=140

How to define "closeness" here?

color_distance_from() {
  awk -v c1="$1" -v c2="$2" '
    BEGIN {
      split(c1, a, ",");
      split(c2, b, ",");
      print abs(a[1] - b[1]) + abs(a[2] - b[2]) + abs(a[3] - b[3]);
    }
    function abs(x) { return ((x < 0) ? -x : x) }
  '
}

They're using Manhattan distance in RGB space. Manhattan distance isn't the most perceptually accurate way to measure color similarity2, but it works great here because the colors are so uniform.

Manhattan distance just adds up the absolute differences between each RGB channel. So comparing (255,0,0) red to (0,255,0) green gives you |255-0| + |0-255| + |0-0| = 510. The maximum possible distance between any two RGB colors is 765 (when comparing pure white to pure black).

This creates a sparse representation where only the most visually important pixels survive the filtering process. The blue represents the border of the ghostty logo elements, while white captures the inner part of the ghost. Everything else (black background and back inner border) becomes empty space that we just won't fill with ascii text.

An aside on luminance

Time for some light color theory. Luminance is the measure of how bright a color appears to human vision. This isn't the same as simply averaging RGB values within some region because in practice our eyes don't perceive all colors as equally bright.

Human vision has three types of cone cells that respond to different wavelengths of light - roughly corresponding to red, green, and blue. But they're not equally sensitive. We're most sensitive to green light, moderately sensitive to red, and least sensitive to blue. This is why a pure green (0,255,0) appears much brighter than a pure blue (0,0,255) even though they have the same "intensity" in digital terms.

The standard formula for relative luminance weights these sensitivities:

L = 0.2126 × R + 0.7152 × G + 0.0722 × B

These coefficients come from the CIE 1931 color space, which was developed by measuring how observers perceive brightness across different wavelengths. The green coefficient (0.7152) is about 10x larger than blue (0.0722), reflecting our much higher sensitivity to green light.3

For terminal animations, this is crucial because ASCII characters represent different visual densities. A . character appears much lighter than a @ character when stacked next to one another, so you want to map dim colors to light characters and bright colors to heavy characters. Without proper luminance calculation, a bright blue might incorrectly map to a heavy character when it should appear relatively dim.

Character selection

Once a pixel passes the color filter, the script maps it to an ASCII character based on relative luminance. The luminance calculation uses those perceptual weighting coefficients from CIE 1931:

pixel_for() {
  local luminance=$(awk -v r="$r" -v g="$g" -v b="$b" 'BEGIN{print int((0.2126 * r + 0.7152 * g + 0.0722 * b) / 1)}')

  local blue_distance="$(color_distance_from "$BLUE" "$1")"
  local white_distance="$(color_distance_from "$WHITE" "$1")"

  if [[ $blue_distance -lt $BLUE_DISTANCE_TOLERANCE ]]; then
    local scaled_luminance=$(awk -v luminance="$luminance" -v min="$BLUE_MIN_LUMINANCE" -v max="$BLUE_MAX_LUMINANCE" 'BEGIN{print int((luminance - min) * 9 / (max - min))}')
    echo "B$scaled_luminance"
  elif [[ $white_distance -lt $WHITE_DISTANCE_TOLERANCE ]]; then
    local scaled_luminance=$(awk -v luminance="$luminance" -v min="$WHITE_MIN_LUMINANCE" -v max="$WHITE_MAX_LUMINANCE" 'BEGIN{print int((luminance - min) * 9 / (max - min))}')
    echo "W$scaled_luminance"
  else
    echo " "
  fi
}

There's a bit of minmax thresholding going on here too. Blue pixels are constrained to luminance values 10-21, while white pixels use 165-255. This ensures that:

  • Dark blue UI elements map to "lighter" ASCII characters (·, ~, o)
  • Bright white text maps to "heavier" characters (*, %, $, @)
  • Maximum contrast is preserved within each color category

The final character mapping happens through a series of sed substitutions:

sed 's/0/·/g' \
| sed 's/1/~/g' \
| sed 's/2/o/g' \
| sed 's/3/x/g' \
| sed 's/4/+/g' \
| sed 's/5/=/g' \
| sed 's/6/*/g' \
| sed 's/7/%/g' \
| sed 's/8/$/g' \
| sed 's/9/@/g'

This progression from · to @ represents increasing visual density to effectively represent luminance in a character-based display.

Aspect ratio

Terminal characters typically have an aspect ratio around 0.44 (width/height). Without correction, the animation would appear vertically stretched. The script handles this by pre-squishing each frame:

FONT_RATIO=".44"
local new_height=$(echo "$FONT_RATIO * $image_height" | bc | jq '.|ceil')
magick "$f" -resize "x$new_height"'!' "$squished_image_file"

The ! in ImageMagick's resize command forces the exact dimensions, ignoring the original aspect ratio. This ensures that when the squished image gets displayed using terminal characters, it appears with the correct proportions.

Processing pipeline

This whole processing is handled by ffmpeg. They're effectively just using it as a mp4 to png frame converter, where each frame can then be passed through the ascii conversion pipeline.

generate_frame_images() {
  # Extract frames with ffmpeg
  ffmpeg \
    -loglevel error \
    -i "$video_file" \
    -vf "scale=$OUTPUT_COLUMNS:-2,fps=$OUTPUT_FPS" \
    "$frame_images_dir/frame_%04d.png"

  # Process each frame individually
  for f in $(find "$frame_images_dir" -name '*.png' | sort); do
    # Aspect ratio correction
    magick "$f" -resize "x$new_height"'!' "$squished_image_file"
    
    # Convert to RGB text data
    magick "$f" "$imagemagick_text_file"
    
    # Process each pixel
    cat "$imagemagick_text_file" | tail -n +2 | while read line; do
      local rgb="$(echo "$line" | cut -f2 -d ' ' | cut -d "(" -f 2 | cut -d ")" -f1)"
      local pixel="$(pixel_for "$rgb")"
      echo -n "$pixel" >> "$output_text_file"
    done
    
    # Apply character mapping and HTML formatting
    cat "$output_text_file" \
      | perl -pe 's/(B[0-9](?:B[0-9])*)/<span class="b">\1<\/span>/g' \
      | sed 's/B//g' \
      # ... more transformations
    
    # Clean up intermediate files
    rm "$f" "$imagemagick_text_file"
  done
}

HTML output

The final transformation wraps consecutive blue characters in <span class="b"> tags for CSS styling4:

perl -pe 's/(B[0-9](?:B[0-9])*)/<span class="b">\1<\/span>/g'

This allows the web page to style blue characters differently from white ones, maintaining the visual distinction that was established during color filtering. The result is HTML that looks like terminal output but can be styled with CSS for better web presentation.

Conclusion

I thought this approach was a pretty clever way to grab some attention for a terminal client. It's certainly different - you barely see ascii on the web these days and you never see ascii video.

If we're really all moving to the terminal maybe that will soon change.

  1. You might recognize that last name: he's the cofounder of Hashicorp which makes terraform and a lot of other devops libraries. ↩

  2. The gold standard for color distance is Delta E, which measures perceptual color difference. Unlike RGB, LAB is perceptually uniform, so a change of 1 unit should look like the same amount of difference regardless of where you are in the color space. But the math is non-trivial and almost certainly overkill for ASCII art. ↩

  3. This incidentally is used frequently in grayscale conversion to preserve the apparent brightness relationships from the original color image. ↩

  4. By george! A perl script in the wild. You don't see those every day. ↩