All work

photoGIF

One photo, a full set of animated WeChat stickers

Web

The workflow going around online has three steps: generate a 3×3 grid with an AI image model, cut out the background with bgsub, export a GIF from a video editor. It has three failure modes you can’t avoid — the animation’s background turns grey, the export goes over 1 MB and WeChat refuses it, and the AI watermark won’t come off.

This site collapses those three steps into one, and skips the manual grid-slicing and manual cut-out along the way. Apart from the AI generation step, matting, animation and compression all happen in your own browser — the photo never goes to a server.

What each failure mode actually is

Problem Cause What happens here
Grey background Video codecs carry no alpha, so transparency flattens to grey No video at all — transform the transparent sticker frame by frame and encode straight to a transparent GIF
Over 1 MB The video editor knows nothing about WeChat’s sticker limits Treat 1 MB as an input constraint and step down through a compression ladder automatically
Watermark won’t come off It shares an image with the subject Connected-component filtering by area — the watermark is a small fragment and gets dropped

Matting without a model

Forcing “pure black background, white outline, non-touching” into the prompt isn’t an aesthetic choice — it’s the input contract for the whole image pipeline. Given that, no matting model is needed at all:

  • Flood-fill inward from the canvas edges to find the background, rather than thresholding on global brightness. A global threshold takes the subject’s black hair, black clothes and black pupils with it — the single most common way this breaks. A flood fill can’t reach inside the subject, so interior black survives by construction.
  • Estimate alpha from the compositing model. With a pure black background, an anti-aliased edge pixel is observed as α·F. A fixed brightness ramp is wrong here: a white outline pixel at 50% coverage reads as grey 128, and the ramp calls that fully opaque — leaving a grey fringe around the sticker that gives itself away the moment the chat background is white. The right move is to take the brightness of interior neighbours as the reference F and solve α = observed / reference.
  • Undo the darkening: F = C / α restores edge colour that the background pulled down. Measured, mean edge brightness recovers from 138 to 255.
  • One connected-component pass does two jobs: splitting the 3×3 grid into nine separate stickers, and discarding any component too small to be one (watermarks, noise).

Animation and encoding

Twelve procedural effects, every one guaranteeing f(0) === f(1) so loops don’t jump. The renderer computes the animation’s full envelope before choosing the sticker’s canvas size — otherwise the frame at the top of a bounce gets clipped.

Encoding runs on gifenc in a Web Worker. Over budget, it steps down a ladder: colours first, then dimensions, frames last. The order matters — stickers are flat colour blocks with an outline, so dropping to 64 colours is nearly invisible, while dropping frames makes the motion visibly stutter. After frame-dropping, per-frame delays are scaled back up so the motion keeps its original speed.

Measured on a 900×900 grid: matting and splitting into nine, 169 ms; encoding and exporting all nine GIFs, 821 ms; output 240×240 at 35–65 KB, transparency flag per frame, with a Netscape looping block.

Known limits

  • No HEIC. Browsers can’t decode the iPhone default, so for now it prompts you to convert. Supporting it means pulling in libheif-wasm (about 1 MB, worth lazy-loading).
  • GIF has 1-bit alpha, so semi-transparent edges get hard-cut. The white outline in the prompt hides this; a sticker without one shows visible jaggies.
  • If the model ignores the “non-touching” instruction, two stickers become one connected component and get cut together. Lowering the fragment-filter threshold and fixing it by hand is the workaround.

How it’s built

React, TypeScript and Vite, deployed on Cloudflare Workers. The only server-side code is a single Worker whose sole reason to exist is hiding the image-generation API key — the photo goes in, gets forwarded, the result comes back, and nothing is stored or logged. Everything else is computed in the browser.

photoGIF — 1
photoGIF — 2
Launched
2026
My role
Solo developer
Built with
ReactTypeScriptWeb WorkerCloudflare Workers