ASCII CAM

Real-time ASCII art from your webcam

Vanilla JS Canvas 2D Web Workers MediaRecorder 4 Render Modes No Dependencies
[ Launch Demo ] [ Learn More ] [ GitHub ]

LIVE DEMO

ABOUT

// what is ascii cam and why does it exist?

ASCII Cam started as a Python experiment: could I turn a live webcam feed into something that looked like it came straight out of a 1980s mainframe terminal? The answer was a resounding yes, and the result was far more fun than expected.

The original version used OpenCV for camera capture and Sobel edge detection, Pillow for font rendering, and NumPy for fast array operations. It shipped with four distinct render modes and a full keyboard-driven settings interface.

This web port brings the same pipeline to the browser — zero dependencies, zero build step, just vanilla JavaScript running inside a Web Worker so the ASCII conversion never blocks your UI thread. All four modes are faithfully ported, and new features like video recording, static image upload, and shareable settings URLs are added on top.

The page you are reading right now is the portfolio. It documents the project, explains the algorithm, lists every library used, and links to the source — while also being a live, interactive demo.

[ GitHub Profile ] [ OG Python Repo ]

HOW IT WORKS

// the pipeline from pixels to characters

📷
Camera
Downsample
🔲
Grayscale
Sobel
A
Char Map
🖼
Canvas

Camera Capture & Downsample

Each animation frame, the live video is drawn onto a tiny <canvas> scaled to exactly the character grid size (e.g. 100 × 56 pixels for a 100-column grid). This gives us one pixel per character cell — identical to OpenCV's cv2.resize() in the Python version.

Grayscale Conversion

The raw RGBA ImageData is converted to grayscale using the standard BT.601 luminance formula Y = 0.299R + 0.587G + 0.114B. This produces a flat Uint8ClampedArray that the Sobel kernels can run on efficiently. All this happens inside a Web Worker to keep the main thread free.

Character Mapping (LUT)

A 256-entry lookup table is prebuilt from the active character set. Each pixel's brightness [0 – 255] indexes directly into the LUT for an O(1) character lookup per pixel. This is equivalent to Python's int(pixel / (255 / (len−1))) formula, but compiled once.

Sobel Edge Detection

A hand-written 3 × 3 Sobel convolution computes horizontal and vertical gradients (Gx, Gy). The magnitude √(Gx² + Gy²) and direction arctan2(Gy, Gx) determine which edge character (| - / \) replaces a brightness character for pixels above the threshold.

Non-Maximum Suppression

In Refined Edge mode, each pixel is only kept as an edge if it is a local maximum along its gradient direction (0°/45°/90°/135°). This thins double-pixel edges down to single-pixel lines, producing far cleaner outlines — the same algorithm as Canny's first thinning step.

Colour Extraction & Saturation

In Colour mode, the original (pre-grayscale) RGBA data is divided into a grid of cells. The mean RGB of each cell drives the fillStyle for that character. Saturation is enhanced with an RGB → HSL → RGB conversion before averaging, matching PIL.ImageEnhance.Color.

RENDER MODES

// four ways to see the world in ASCII

Basic ASCII

Maps pixel brightness to characters by density. Bright pixels → sparse chars (. :), dark pixels → dense chars (@ #). Pure brightness, no edge information.

Edge Detection

Runs a 3 × 3 Sobel kernel on each frame. Pixels exceeding the magnitude threshold are replaced with directional characters (| - / \) that follow the gradient. Everything below threshold uses brightness mapping.

Refined Edge

Adds non-maximum suppression after Sobel — each edge pixel is kept only if it is the local maximum along its gradient direction. Produces single-pixel-wide outlines for sharper, cleaner contours.

Colored ASCII

Combines edge detection with per-character colour sampling. The average RGB of each character cell in the source frame drives the canvas fillStyle. Saturation is boosted before sampling for vivid output.

TECH STACK

// what powers the web version and the original python version

// Web Version

Canvas 2D API
Renders the ASCII character grid frame-by-frame using fillText(). Per-character colour is applied with fillStyle in Colour mode. toBlob() powers PNG snapshot downloads.
📹
getUserMedia API
Streams live webcam video via navigator.mediaDevices.getUserMedia(). A hidden <video> element receives the stream and feeds frames to the offscreen canvas.
Web Workers
All pixel processing — grayscale conversion, Sobel convolution, character mapping, colour sampling — runs off the main thread via an ES module Web Worker. Zero jank during heavy processing.
🔴
MediaRecorder API
Records the ASCII canvas stream at 30 FPS, preferring VP9 WebM. On stop, chunks are assembled into a Blob and downloaded via a synthetic anchor click. No server required.
🔗
URL / History API
Every settings change updates the URL query string via history.replaceState() without reloading. Pasting the URL in a new tab restores the exact session — shareable links at zero cost.

// Python Version

👁
OpenCV (cv2)
Captures webcam frames with VideoCapture, performs Sobel edge detection with cv2.Sobel(), and handles real-time display via imshow(). Also writes AVI recordings with XVID codec.
🖼
Pillow (PIL)
Renders each ASCII string into a proper bitmap using ImageDraw.text() with a TrueType font. Also provides ImageEnhance.Color for saturation boosting in Colour mode.
Σ
NumPy
Powers fast vectorised pixel operations: flattening image arrays, computing per-cell colour averages with np.mean(), and performing element-wise threshold comparisons at near-C speed.
🐍
Python Dataclasses & Enums
Settings are stored in a @dataclass for clean default management. Render modes use an Enum for type-safe switching. Font instances are cached in a dict to avoid re-loading per frame.

SOURCE CODE

// open source — go build something weird with it

JavaScript · Vanilla

ascii-cam-web

This page. Canvas 2D + Web Worker ASCII pipeline, four render modes, WebM recording, PNG snapshot, static image upload, and shareable settings URLs. No build step, no dependencies — open index.html on a local server and go.

⬡ github.com/salman-m-498/ascii-cam-web →
Python 3.8+

asciicam (original)

The original Python project that started it all. OpenCV for capture and edge detection, Pillow for rendering, NumPy for number crunching. Four render modes, a full keyboard control interface, real-time FPS overlay, and AVI recording.

⬡ github.com/salman-m-498/asciicam →
// author
Salman Moosa
Building things at the intersection of systems, graphics, and whatever seems fun. Open to new opportunities.
GitHub