Turn a photo into string art — a single black thread wound around nails on a circular frame — and preview the result before you pick up a hammer.
Everything runs in the browser: a fully static app with the solver compiled from Rust to WebAssembly. No backend, no uploads — your photos never leave the page.
- Upload a photo and frame it inside the circle (drag to pan, scroll to zoom).
- The image is converted to a darkness map: grayscale, optional local contrast boost, percentile autocontrast, gamma — shown live on the middle B&W canvas.
- A greedy least-squares solver picks the nail sequence: at each step it tries every chord from the current nail and keeps the one that reduces the squared error between the simulated canvas and the target the most. Thread lines are anti-aliased (Xiaolin Wu) on a small work canvas, so the simulation matches what real overlapping thread does.
- Optionally, paint an importance mask right on the B&W canvas: purple zones are "don't care" — the solver neither rewards nor penalizes thread there, spending its lines on the areas that matter (faces, eyes) and stopping early once nothing important is left to improve. Brush/eraser with size and opacity control; the mask sticks to the photo, so re-cropping keeps it aligned.
- The sequence is rendered honestly, thread by thread, with accumulating darkness and slight nail jitter to avoid moiré — the preview is what the physical piece would look like.
Solves run in a Web Worker, so the UI stays responsive; results and settings persist in IndexedDB across page reloads.
| Group | Slider | What it does |
|---|---|---|
| Image | Gamma | tone curve of the darkness map |
| Image | Detail | local contrast (unsharp mask) |
| String art | Nails | nails around the circle |
| String art | Max lines | thread count budget |
| String art | Thread weight | how much darkness one thread adds |
| String art | Tone floor | lifts the target's black point — saves thread on large dark areas |
| String art | Preview size | output resolution only (no re-solve) |
Prerequisites: Node.js, Rust with the wasm32-unknown-unknown target, and
wasm-pack.
# build the WASM solver
cd solver-rs
wasm-pack build --target web --out-dir ../src/solver/pkg
# run the app
cd ..
npm install
npm run devnpm run build produces a fully static site in dist/ — deployable to any
static host (GitHub Pages, Cloudflare Pages, …).
src/— TypeScript UI: crop, preprocessing, sliders, worker scheduling, IndexedDB persistencesrc/solver/— worker + thin wrapper around the WASM modulesolver-rs/— the solver and renderer in Rust (compiled to ~31 KB of WASM)
The solver approach follows kaspar98/StringArt.