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