fast-pixelizer
Fast, zero-dependency image pixelation library. Works in browser and Node.js.

Snap Mode — Turn Fake Pixel Art into Real Pixel Art
Most pixel art you find online is broken — scaled up with blurry interpolation, anti-aliased edges, and misaligned grids. snap() automatically detects the original pixel grid and rebuilds it with perfectly uniform cells.
As of 1.2.0, repeated snap() runs stay stable on already-clean images and square canvases no longer drift into mismatched X/Y grids.
As of 1.3.1, high-resolution generated images are also evaluated against model and demo fixtures so the detector avoids snapping to tiny texture noise while keeping already-uniform snap outputs idempotent.
Recent regression examples:
1.gemini.png | 201x201 | 200x200 |
2.well-converted.png | 65x69 on re-snap | 201x201 |
3.gpt.png | 148x150 | 148x148 |
4.gpt.png | 98x179 | 97x97 |
How it works:
- K-means++ color quantization — reduces noise to make grid edges detectable
- Edge profile analysis — scans horizontal & vertical color boundaries
- Periodicity detection — recovers the repeating cell size even when visible boundaries are sparse
- High-resolution plausibility guard — avoids treating 1–2px generated texture as the real grid
- Uniform-cell detection — preserves already-snapped square-cell outputs on repeated runs
- Lattice regularization — rebuilds a globally uniform grid instead of letting local cut drift accumulate
- Majority-vote resampling — picks the dominant color per cell
- Uniform re-rendering — every cell gets the exact same pixel size
No manual resolution input needed. The grid is auto-detected.
Input quality note
snap() works best when the source image really came from a low-resolution square grid that was later scaled up.
ChatGPT-generated "pixel art" often bakes the inconsistency into the source image itself: some cells are already wider, taller, softer, or slightly off-axis before snap() ever sees them. In those cases snap() can regularize the output and force it back onto a square lattice, but it cannot perfectly recover information that was never on a clean grid to begin with, so quality is not guaranteed.
If you are generating new source images specifically for snap(), prefer Nano Banana or any workflow that preserves a true square low-res lattice from the start.
Snap quality eval
Run the model/demo quality loop with:
npm run eval:snap-quality
The eval writes summary.json, summary.md, snapped images, and resized grid images under .tmp/snap-quality-eval/.
Current criteria include:
| Aspect preservation | Crops or grids whose cell aspect drifts from the source ratio |
| Micro-grid snap | Detectors that lock onto generated texture instead of cells |
| Idempotence | snap(snap(image)) changing the detected grid |
| Boundary evidence | Weak inferred boundaries compared with average image gradients |
| Source disorder | Images whose inferred cells are internally noisy or painterly |
| Preservation | Excessive nearest-neighbor MAE against the original image |
The June 2026 model/demo fixture run improved from 5 fail / 5 pass / 6 review with total repeat gap 273 to 0 fail / 11 pass / 5 review with repeat gap 0.
import { snap } from 'fast-pixelizer'
const result = snap(imageData)
Pixelate Mode — Generate Pixel Art from Any Image
clean — picks the most frequent color in each cell. Sharp, graphic pixel art look.
detail — averages all colors in each cell. Smoother gradients, more texture.
import { pixelate } from 'fast-pixelizer'
const result = pixelate(imageData, { resolution: 32 })
Install
npm install fast-pixelizer
Usage
The input accepts a browser ImageData, a node-canvas image data object, or any plain { data: Uint8ClampedArray, width: number, height: number }.
Browser
import { pixelate, snap } from 'fast-pixelizer'
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
ctx.drawImage(myImage, 0, 0)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const result = pixelate(imageData, { resolution: 32 })
const repaired = snap(imageData)
const out = new ImageData(result.data, result.width, result.height)
ctx.putImageData(out, 0, 0)
Node.js (with sharp)
import sharp from 'sharp'
import { pixelate, snap } from 'fast-pixelizer'
const { data, info } = await sharp('./photo.png')
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
const input = {
data: new Uint8ClampedArray(data.buffer),
width: info.width,
height: info.height,
}
const result = pixelate(input, { resolution: 32 })
const repaired = snap(input, { colorVariety: 64 })
await sharp(Buffer.from(result.data), {
raw: { width: result.width, height: result.height, channels: 4 },
})
.png()
.toFile('./output.png')
API
snap(input, options?): SnapResult
Detects the pixel grid in existing pixel art and re-snaps it to a clean, uniform grid.
options: SnapOptions
colorVariety | number | 32 | K-means color count. Higher = more detail, slower detection. |
output | 'original' | 'resized' | 'original' | 'original' = uniform grid at ~original size. 'resized' = grid-sized. |
SnapResult
interface SnapResult {
data: Uint8ClampedArray
width: number
height: number
detectedResolution: number
colCuts: number[]
rowCuts: number[]
}
pixelate(input, options): PixelateResult
input: ImageLike
interface ImageLike {
data: Uint8ClampedArray
width: number
height: number
}
Compatible with the browser's built-in ImageData, node-canvas, and raw pixel buffers.
options: PixelateOptions
resolution | number | { cols, rows } | required | Grid size. 32 keeps legacy 32×32 output. { cols, rows } enables rectangular grids. |
mode | 'clean' | 'detail' | 'clean' | 'clean' = most-frequent color per cell. 'detail' = average color per cell. |
output | 'original' | 'resized' | 'original' | 'original' = same dimensions as input. 'resized' = output is grid-sized. |
PixelateResult
interface PixelateResult {
data: Uint8ClampedArray
width: number
height: number
}
For non-square crops, use fitResolutionToAspect(input, n) to turn a single scalar into a rectangular grid while keeping square cells:
const grid = fitResolutionToAspect(imageData, 64)
const result = pixelate(imageData, {
resolution: grid,
output: 'resized',
})
Examples
snap(img)
snap(img, { colorVariety: 64, output: 'resized' })
pixelate(img, { resolution: 32 })
pixelate(img, { resolution: { cols: 80, rows: 32 } })
pixelate(img, { resolution: 64, mode: 'detail', output: 'resized' })
Try it locally
Clone the repo and run the library against the sample image to see the output yourself:
git clone https://github.com/handsupmin/fast-pixelizer.git
cd fast-pixelizer
npm install
npm run examples
Output images will be written to examples/. Replace docs/original.png with any image to try your own.
Performance
pixelate | 32 | 512×512 | ~1ms |
pixelate | 128 | 512×512 | ~3ms |
pixelate | 256 | 1024×1024 | ~12ms |
snap | auto | 512×512 | ~50ms |
snap | auto | 1024×1024 | ~150ms |
pixelate: pre-allocated Uint16Array(32768) bucket table — no Map, no per-call heap allocations.
snap: K-means++ quantization + periodicity-guided grid recovery. Heavier than pixelate but still fast enough for real-time use.
- Cell boundaries use
Math.round to eliminate pixel gaps and overlaps between adjacent cells.
- Both functions iterate in row-major order for CPU cache locality.
- Zero runtime dependencies.
Web Worker (browser)
For large images, run inside a Worker to keep the main thread unblocked:
import { pixelate, snap } from 'fast-pixelizer'
self.onmessage = (e) => {
const { input, options, mode } = e.data
const result = mode === 'snap' ? snap(input, options) : pixelate(input, options)
self.postMessage(result, [result.data.buffer])
}
const worker = new Worker(new URL('./pixelate.worker.ts', import.meta.url), { type: 'module' })
worker.postMessage({ input, options, mode: 'snap' }, [input.data.buffer])
worker.onmessage = (e) => console.log(e.data)
Contributing
Contributions are welcome! See CONTRIBUTING.md.
License
MIT