VIDEO TO ASCII: CONVERTING VIDEOS TO ANIMATED ASCII ART

Upload video, customize colors, watch frames convert to ASCII in real-time.

VIDEO PROCESSING 2026.01.01
 

THE PROBLEM

Converting video to ASCII art isn't new. But most implementations are client-side, limited by browser memory and CPU. Quality suffers. Long videos crash. Large files fail.

Solution: Client-side processing using Canvas API for frame extraction and HTML text rendering for crisp output. Luminance-based character mapping with pixel region averaging. Responsive, embeddable ASCII video component.

TRY IT NOW

No limit - higher = more detail

Font Size Controls

0.5x 3x

ARCHITECTURE

The system uses a client-side approach with Canvas API for frame extraction. Each video is processed entirely in the browser, storing frames as ASCII string arrays. HTML text rendering displays frames using requestAnimationFrame for smooth 30fps playback with crisp, scalable output.

Why client-side? Processing happens entirely in the browser, ensuring privacy (files never leave the device) and providing immediate feedback. The Canvas API extracts frames from video, and HTML text rendering displays crisp, scalable ASCII output.

Quality optimizations: Extended ASCII character set (70+ chars), luminance-based mapping with pixel region averaging, configurable density (10-300 chars), color customization, and precise font rendering with measured character spacing.

HOW IT WORKS

Frame extraction: The component creates a hidden HTML video element and uses the Canvas API to extract frames. It seeks through the video at 30fps intervals, drawing each frame to a temporary canvas element. This leverages the browser's native video decoding capabilities.

Processing flow: Each frame is converted to ASCII using luminance-based character mapping. The processed frames are stored as string arrays in memory. A requestAnimationFrame loop displays frames at 30fps, creating smooth animation.

// Client-side video processing
async function processVideo(
  file: File,
  bgColor: string,
  textColor: string,
  density: number
) {
  // Create hidden video element
  const video = document.createElement("video");
  video.src = URL.createObjectURL(file);
  await video.load();

  const fps = 30;
  const frameInterval = 1 / fps;
  const totalFrames = Math.floor(video.duration * fps);
  const asciiFrames: string[] = [];

  // Create temporary canvas for frame extraction
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  // Extract and convert each frame
  for (let i = 0; i < totalFrames; i++) {
    video.currentTime = i * frameInterval;
    await new Promise((resolve) => {
      video.onseeked = resolve;
    });

    // Draw frame to canvas
    ctx.drawImage(video, 0, 0);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Convert to ASCII
    const ascii = convertFrameToASCII(imageData, density);
    asciiFrames.push(ascii);

    // Update progress
    updateProgress(i + 1, totalFrames);
  }

  // Start animation
  startAnimation(asciiFrames, bgColor, textColor);
}

function updateProgress(current: number, total: number) {
  console.log(`Processing frame ${current}/${total}`);
}

function startAnimation(
  frames: string[],
  bgColor: string,
  textColor: string
) {
  const display = document.getElementById("ascii-display")!;
  let frameIndex = 0;

  function animate() {
    display.textContent = frames[frameIndex];
    frameIndex = (frameIndex + 1) % frames.length;
    requestAnimationFrame(animate);
  }

  animate();
}

ASCII CONVERSION

Luminance mapping: Each pixel region is converted to a single ASCII character based on its luminance value. The formula 0.299*R + 0.587*G + 0.114*B accurately represents perceived brightness, matching how human eyes perceive grayscale.

Quality improvements: Instead of sampling a single pixel per character, we average multiple pixels across a region (4-9 pixels depending on density). This reduces noise and produces smoother gradients. The extended character set (70+ chars) provides fine-grained luminance steps for high-quality output.

Character selection: Characters are ordered from darkest to lightest. The luminance value (0-255) is normalized and mapped to the character array index, ensuring consistent visual representation across different input images.

// ASCII conversion algorithm
function convertToASCII(
  imageData: ImageData,
  config: { bgColor: string; textColor: string; density: number }
): string {
  const { width, height, data } = imageData;
  const { bgColor, textColor, density } = config;
  
  // Character gradient (70+ characters for quality)
  const chars = " .'^`",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
  
  // Calculate output dimensions
  const charWidth = Math.floor(width / density);
  const charHeight = Math.floor(height / (density * 2)); // Characters are taller
  
  let ascii = "";
  
  for (let y = 0; y < charHeight; y++) {
    for (let x = 0; x < charWidth; x++) {
      // Sample pixel from original image
      const px = Math.floor((x / charWidth) * width);
      const py = Math.floor((y / charHeight) * height);
      const idx = (py * width + px) * 4;
      
      // Calculate luminance (0-255)
      const r = data[idx];
      const g = data[idx + 1];
      const b = data[idx + 2];
      const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
      
      // Map luminance to character
      const charIndex = Math.floor((luminance / 255) * (chars.length - 1));
      ascii += chars[charIndex];
    }
    ascii += "\n";
  }
  
  return ascii;
}

// Render ASCII to canvas with colors
function renderASCII(
  ascii: string,
  canvas: HTMLCanvasElement,
  bgColor: string,
  textColor: string
) {
  const ctx = canvas.getContext("2d")!;
  ctx.fillStyle = bgColor;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  ctx.fillStyle = textColor;
  ctx.font = "12px monospace";
  ctx.textBaseline = "top";
  
  const lines = ascii.split("\n");
  lines.forEach((line, y) => {
    ctx.fillText(line, 0, y * 12);
  });
}

CLIENT IMPLEMENTATION

Frame extraction: The client creates a hidden video element, seeks through the video at 30fps intervals, and draws each frame to a temporary canvas. This approach leverages the browser's native video decoding capabilities.

HTML text rendering: Processed ASCII frames are stored as string arrays. A requestAnimationFrame loop updates a <pre> element at 30fps, creating smooth animation. Native HTML text rendering ensures crisp, scalable output that works as an embeddable component.

// Client-side video processing
async function processVideo(
  file: File,
  bgColor: string,
  textColor: string,
  density: number
) {
  // Create hidden video element
  const video = document.createElement("video");
  video.src = URL.createObjectURL(file);
  await video.load();

  const fps = 30;
  const frameInterval = 1 / fps;
  const totalFrames = Math.floor(video.duration * fps);
  const asciiFrames: string[] = [];

  // Create temporary canvas for frame extraction
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  // Extract and convert each frame
  for (let i = 0; i < totalFrames; i++) {
    video.currentTime = i * frameInterval;
    await new Promise((resolve) => {
      video.onseeked = resolve;
    });

    // Draw frame to canvas
    ctx.drawImage(video, 0, 0);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Convert to ASCII
    const ascii = convertFrameToASCII(imageData, density);
    asciiFrames.push(ascii);

    // Update progress
    updateProgress(i + 1, totalFrames);
  }

  // Start animation
  startAnimation(asciiFrames, bgColor, textColor);
}

function updateProgress(current: number, total: number) {
  console.log(`Processing frame ${current}/${total}`);
}

function startAnimation(
  frames: string[],
  bgColor: string,
  textColor: string
) {
  const display = document.getElementById("ascii-display")!;
  let frameIndex = 0;

  function animate() {
    display.textContent = frames[frameIndex];
    frameIndex = (frameIndex + 1) % frames.length;
    requestAnimationFrame(animate);
  }

  animate();
}

QUALITY OPTIMIZATIONS

  • Extended character set: 70+ characters ordered by visual density for smooth gradients
  • Luminance mapping: 0.299*R + 0.587*G + 0.114*B formula matches human eye perception
  • Pixel region averaging: Samples 4-9 pixels per character region instead of single pixel, reducing noise
  • Configurable density: 10-300 characters wide (tiny to high-quality), default 150
  • Crisp HTML text rendering: Native browser text rendering with optimized font smoothing for scalable output
  • Color customization: Background and text colors applied via CSS
  • Embeddable component: Self-contained HTML snippet with responsive sizing via ResizeObserver
  • Smooth animation: 30fps playback via requestAnimationFrame with frame timing control

LIMITATIONS

  • Browser memory: Large videos may cause performance issues (processing happens entirely client-side)
  • Processing time: Depends on video length and density (all processing happens in browser)
  • Formats: MP4, WebM, MOV (browser-supported formats)
  • Privacy: Files never leave the device (pro, but limits processing power)
Get new posts by email.
Join 6 other subscribers.