Transform any image into generative stipple art using weighted Lloyd relaxation — entirely in the browser.
Upload any image and the stippler analyses its brightness to produce a distribution of points that mirrors the original's light and dark regions — dense stippling in dark areas, sparse in bright ones. The result can be rendered in three modes:
| Mode | Description |
|---|---|
| Dots | Classic stipple portrait — thousands of fine dots |
| Delaunay | Triangulated mesh connecting every point |
| Voronoi | Dual diagram partitioning space around each point |
All computation runs inside a Web Worker so the UI stays completely responsive during the relaxation process. The final result can be exported as a full-resolution PNG.
nhasan143.github.io/delaunay-stippler
- 📁 Drag & drop or file-picker image upload (any browser-supported format)
- ⚙️ Adjustable point count — 200 to 8,000 stipple points via a slider
- 🔄 Three render modes — Dots, Delaunay triangulation, Voronoi diagram
- 🧵 Non-blocking Web Worker — Lloyd relaxation runs off the main thread
- 🎞️ Live preview — canvas updates after every relaxation iteration
- 💾 PNG export — download the finished artwork at full canvas resolution
- 🌐 Zero backend — everything runs client-side; no data ever leaves your device
- 📦 Tiny bundle — single dependency (
d3-delaunay), built with Vite
When an image is uploaded it is drawn onto an offscreen <canvas>. The raw pixel data is read and each pixel's red channel is inverted (density = 1 − r/255) to create a floating-point density map where dark pixels have high weight and bright pixels have low weight.
Points are seeded via rejection sampling biased by the density map. The algorithm then iterates:
- Build a Delaunay triangulation of the current point set using
d3-delaunay. - For every pixel, find the nearest point and accumulate its weighted centroid.
- Move each point toward its weighted centroid with a 1.8× over-relaxation factor plus a small amount of decaying noise to avoid grid artefacts.
- Repeat for 80 iterations, broadcasting a snapshot after each one.
This produces a distribution that visually mirrors the tonal structure of the original image.
The main thread receives each snapshot and redraws the canvas using either:
- Raw
arc()calls for dot mode, or d3-delaunay's built-inrender()/voronoi.render()path helpers for the mesh modes.
| Tool | Role |
|---|---|
| Vite 5 | Dev server & production bundler |
| d3-delaunay 6 | Delaunay triangulation & Voronoi tessellation |
| Web Workers API | Off-thread Lloyd relaxation |
| Canvas API | Image decoding, density extraction & rendering |
| GitHub Actions | CI/CD — auto-deploy to GitHub Pages on push |
- Node.js v18 or later
- npm v9 or later
# 1. Clone the repository
git clone https://github.com/NHasan143/delaunay-stippler.git
cd delaunay-stippler
# 2. Install dependencies
npm install
# 3. Start the dev server
npm run devOpen http://localhost:5173 in your browser.
npm run build # outputs to /dist
npm run preview # locally preview the production builddelaunay-stippler/
├── .github/
│ └── workflows/
│ └── deploy.yml # GitHub Actions — build & deploy to Pages
├── src/
│ ├── main.js # UI wiring, image decode, canvas rendering
│ ├── worker.js # Lloyd relaxation (runs in Web Worker)
│ └── style.css # App styles
├── index.html # Single-page shell
├── vite.config.js # Vite config (sets base path for GitHub Pages)
├── package.json
├── package-lock.json
└── README.md
You can tweak the following constants in src/main.js and src/worker.js:
| Parameter | Location | Default | Description |
|---|---|---|---|
iters |
main.js |
80 |
Number of Lloyd relaxation iterations |
max canvas width |
main.js |
900px |
Images wider than this are scaled down |
| Over-relaxation factor | worker.js |
1.8 |
Values > 1 converge faster but may overshoot |
| Noise decay | worker.js |
(k+1)^-0.8 × 10 |
Controls how quickly random jitter fades out |
| Point range | index.html |
200 – 8000 |
Editable via the slider min/max attributes |
The project deploys automatically to GitHub Pages via GitHub Actions whenever a commit is pushed to main.
To set it up on a fork:
- Go to Settings → Pages in your repository.
- Set the Source to GitHub Actions.
- Push any commit to
main— the workflow handles the rest.
Make sure
vite.config.jshasbaseset to your repo name:base: "/your-repo-name/",
Contributions are welcome! To get started:
# Fork the repo, then:
git checkout -b feature/your-feature-name
# make your changes
git commit -m "feat: describe your change"
git push origin feature/your-feature-name
# open a Pull RequestSome ideas for future improvements:
- Luminance-based density (instead of red-channel only)
- Variable dot radius scaled by local density
- SVG export for scalable vector output
- Drag-and-drop image upload
- Color stippling from the original image palette
This project is licensed under the MIT License — see the LICENSE file for details.
[Naymul Hasan]
- GitHub: @NHasan143
- LinkedIn: @naymulhasan143
If you find this project useful, consider leaving a ⭐ on the repository — it helps a lot!# Delaunay / Voronoi Stippler