Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

ASCII Cam

Live demo: camera-to-ascii.netlify.app

Real-time camera to ASCII art, running entirely in the browser. No server, no dependencies, no install — just open index.html. One-shot with Claude Code using Sonnet 4.6 Medium.

 ██████╗ █████╗ ███╗   ███╗███████╗██████╗  █████╗
██╔════╝██╔══██╗████╗ ████║██╔════╝██╔══██╗██╔══██╗
██║     ███████║██╔████╔██║█████╗  ██████╔╝███████║
██║     ██╔══██║██║╚██╔╝██║██╔══╝  ██╔══██╗██╔══██║
╚██████╗██║  ██║██║ ╚═╝ ██║███████╗██║  ██║██║  ██║
 ╚═════╝╚═╝  ╚═╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝

Usage

Open index.html in any modern browser. For phone use, serve it over your local network:

# Python 3
python -m http.server 8080

# Node (npx)
npx serve .

Then visit http://<your-local-ip>:8080 on your phone. Grant camera permission when prompted.

Controls

Button Action
FLIP Toggle front / back camera
SZ Cycle font size: XS → S → M → L
SMPL / DETL Switch character ramp (10-level simple or 70-level detailed)
INV Invert colors (black on white)
FPS Show live frame rate counter

How it works

The conversion runs on every animation frame (~60 fps):

Camera feed
    │
    ▼
drawImage() → tiny canvas (cols × rows px)   ← GPU-accelerated downscale
    │
    ▼
getImageData()                                ← single CPU read
    │
    ▼
per pixel: luminance → LUT → char            ← integer arithmetic, no floats
    │
    ▼
fillText() × rows                             ← one draw call per row

Algorithm optimizations

GPU downscale first — the video frame is drawn to a processing canvas that is exactly cols × rows pixels (one pixel per character cell). The GPU handles all the interpolation; the CPU only sees the final grid.

Pre-built lookup table — on startup (and on ramp change) a 256-entry charLUT maps every possible luminance value directly to a character. The inner loop becomes a simple array lookup with no arithmetic.

Integer luminance — grayscale is computed with the Rec.601 coefficients using only integer multiplies and a bit-shift, avoiding Math.round and floating-point operations:

// R×0.299 + G×0.587 + B×0.114  ≈  (R×77 + G×150 + B×29) >> 8
const lum = (data[i] * 77 + data[i+1] * 150 + data[i+2] * 29) >> 8;

Row-level text rendering — characters for each row are collected into an array and joined into a single string, then rendered with one fillText() call. This keeps total draw calls at rows (~60–100) instead of cols × rows (~7 000+).

DPR-aware canvas — the output canvas is sized in physical pixels (window.innerWidth × devicePixelRatio), keeping text sharp on retina and high-DPI screens.

Character ramps

Two ramps are available:

Simple  (10 levels): ' .:-=+*#%@'
Detailed (70 levels): ' .`'^",:;il!I><~+_-?...MW&8%B@$'

Index 0 maps to the darkest luminance (rendered as space / background), the last index to the brightest. The inverted mode uses a reversed lookup table built at the same time, so switching costs nothing at runtime.

Browser support

Any browser that supports getUserMedia + Canvas 2D — Chrome, Firefox, Safari 11+, Edge. Works on Android and iOS (Safari requires the playsinline attribute on the video element, which is set).

Project structure

camera_to_ascii/
└── index.html    # entire app — HTML + CSS + JS, no dependencies

About

Camera to ASCII. Generated with Claude

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages