A free, client-side photo collage tool that runs entirely in your browser. No uploads, no account, no server — your photos never leave your device.
- Multiple layouts — grid, masonry, strips, freeform, and more
- Reposition photos — pan each image within its cell to frame the perfect shot
- Captions — add text overlays or below-image captions with custom colors
- Styling controls — gap, background color, corner radius, shadows, borders
- High-res export — download PNG at 1×, 2×, 3×, or 4× resolution
- Drag to reorder — rearrange photos in the tray by dragging
- Mobile friendly — fully responsive with touch support
| Layer | Choice |
|---|---|
| Language | Vanilla JS ES modules |
| Styling | Plain CSS with custom properties |
| Rendering | HTML5 Canvas API |
| Build | Vite + vite-plugin-singlefile |
| Hosting | Cloudflare Pages |
No framework. No dependencies at runtime. Builds to a single self-contained index.html.
# Install dev dependencies (Vite only)
npm install
# Start local dev server
npm run dev
# → http://localhost:5173
# Build for production (outputs dist/index.html)
npm run build
# Preview the production build
npm run previewThe project deploys to Cloudflare Pages via Wrangler:
npm run build
# Preview deployment
npx wrangler pages deploy
# Production deployment
npx wrangler pages deploy --branch mainOr connect your repo in the Cloudflare Pages dashboard:
- Build command:
npm run build - Output directory:
dist
/
├── index.html ← Entry point
├── favicon.svg ← App icon
├── css/
│ └── styles.css ← All styles
├── js/
│ ├── app.js ← Orchestrator / entry point
│ ├── config.js ← Layout registry + SVG thumbnails
│ ├── store.js ← Image state (single source of truth)
│ ├── layouts.js ← All layout draw functions
│ ├── renderer.js ← Canvas owner + render loop
│ ├── repoMode.js ← Click-to-select + drag-to-pan
│ ├── trayUI.js ← Photo tray + caption editor
│ ├── sidebarUI.js ← Layout picker sidebar
│ └── export.js ← PNG export
└── wrangler.toml ← Cloudflare Pages config
- Add an entry to
Config.layoutsandConfig.thumbSVGsinjs/config.js - Implement the draw function in
js/layouts.js:
'my-layout'(ctx, canvas, W, options, hiRes) {
const { gap, bg } = options;
const H = 400;
if (!hiRes) { canvas.width = W; canvas.height = H; }
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
const cells = [];
ImageStore.list.forEach((im, i) => {
const x = 0, y = i * H, w = W, h = H;
this.drawImg(ctx, im, x, y, w, h, options);
cells.push({ x, y, w, h, idx: i });
});
return { H, cells };
},Sidebar, hit testing, export, and reposition mode all work automatically.
MIT
