How I Built a Zero-Server Image Editor for 0¥/Month
I needed a simple tool to make stickers for WeChat. Existing online editors either upload your images to their servers (privacy nightmare), require registration, or watermark your output.
So I built 靓图 (LiangTu) — a pure client-side image editor that runs entirely in the browser. No server, no uploads, no account. Just drag, edit, download.
Here's the architecture.
The Stack: Zero Dependencies on the Server
Browser (100% of logic)
├── Canvas API — rendering pipeline
├── gif.js — GIF encoding (Web Workers)
├── FileReader API — local image loading
└── No backend. None.
That's it. The entire app is a static HTML/CSS/JS page served by Nginx. No Node.js, no database, no API routes. The server's job is to ship bytes once and shut up.
What It Does
Three tools, one domain:
| Tool | What it does | Tech |
|---|---|---|
| Sticker Editor | Add text layers, filters, watermarks, crop, compress | Canvas 2D |
| GIF Maker | Drag multiple images → reorder → adjust speed → export animated GIF | gif.js + Web Workers |
| Image Compressor | Quality slider with live preview | Canvas toDataURL |
The sticker editor has 8 tools (crop, text with 14 Chinese fonts, background fill, flip, filters, adjustments, watermark, compress) plus a full layer system with z-ordering, rotation, and drag-to-reposition.
The Layer System
This was the hardest part. Each text element is an independent layer with:
layer = {
text: '你好',
x: canvasW / 2,
y: canvasH - 40,
fontSize: 24,
fontFamily: 'Microsoft YaHei',
textColor: '#FFFFFF',
strokeColor: '#000000',
strokeWidth: 2,
rotation: 0
}
The render loop draws background → layers (top to bottom) → watermark. Hit testing for drag uses ctx.measureText() and rotation-aware bounding boxes. Selection shows a dashed outline with rotation handles.
GIF Encoding in the Browser
For the GIF maker, I use gif.js — a pure JavaScript GIF encoder that runs in Web Workers. Users drag in multiple images, reorder them, set the delay per frame, and hit export.
const gif = new GIF({
workers: 2,
quality: 10,
width: maxWidth,
height: maxHeight,
workerScript: 'gif.worker.js'
});
frames.forEach((frame, i) => {
gif.addFrame(canvas, { delay: speeds[i] * 100 });
});
gif.on('finished', blob => {
const url = URL.createObjectURL(blob);
// show preview + auto-download
});
gif.render();
The trick is normalizing all frames to the same dimensions (largest width × largest height), centering smaller images on a white background.
Why "100% Local" Matters
Every image stays in the browser. The <input type="file"> loads directly into a FileReader, then to an Image object, then to Canvas. The download is canvas.toBlob(). At no point does any pixel leave the user's machine.
For Chinese users especially, uploading photos to unknown servers is a hard no. Making "no upload" the core feature was the right call.
Cost: ~16¥/Month
- Domain: liangtu.cc — 29¥/year
- Server: Tencent Cloud Lighthouse (2 vCPU, 2GB RAM, 4Mbps) — 192¥/year
- Total: ~16¥/month (~$2.20 USD)
No serverless functions, no CDN tiers, no database. A single Nginx server serving static files. The 4Mbps bandwidth handles ~3,000 page views/month with plenty of headroom.
The Traffic Problem
Here's where I'm honest: I built the product. The SEO is decent. But nobody's finding it (~5 real visitors/day).
If you've successfully grown a free tool site from zero, I'd love to hear how. Drop a comment or find me at github.com/fengloulai/liangtu.
Try it: liangtu.cc
Source: github.com/fengloulai/liangtu
Top comments (0)