Real-time ASCII art from your webcam
// 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.
// the pipeline from pixels to characters
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.
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.
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.
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.
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.
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.
// four ways to see the world in ASCII
Maps pixel brightness to characters by density. Bright pixels → sparse chars (. :), dark pixels → dense chars (@ #). Pure brightness, no edge information.
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.
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.
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.
// what powers the web version and the original python version
fillText(). Per-character colour is applied with fillStyle in Colour mode. toBlob() powers PNG snapshot downloads.navigator.mediaDevices.getUserMedia(). A hidden <video> element receives the stream and feeds frames to the offscreen canvas.history.replaceState() without reloading. Pasting the URL in a new tab restores the exact session — shareable links at zero cost.VideoCapture, performs Sobel edge detection with cv2.Sobel(), and handles real-time display via imshow(). Also writes AVI recordings with XVID codec.ImageDraw.text() with a TrueType font. Also provides ImageEnhance.Color for saturation boosting in Colour mode.np.mean(), and performing element-wise threshold comparisons at near-C speed.@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.// open source — go build something weird with it
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.
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 →