Disclosure: I made Purupuru Maker, the browser tool this post is based on.
"Purupuru" (ã·ã‚‹ã·ã‚‹) is the Japanese onomatopoeia for something soft and jiggly, like pudding, cheeks or a jelly cat. The tool does one thing. You paint where a still image should wobble, preview the jelly motion, and export a GIF, MP4 or WebM. All of it runs in the browser tab, with no upload and no account.
Below is how the pipeline works at a conceptual level, plus the browser APIs and libraries visible in the shipped bundle.
The pipeline at a glance
- Decode a static PNG, JPEG or WebP into memory.
- Paint a mask that says how much each region is allowed to move.
- Simulate a soft-body-ish response driven by a motion preset.
- Render the deformed image every frame with WebGL2.
- Record and encode a deterministic loop locally, then hand the user a file.
No step talks to an image-processing server. The privacy page is explicit that the source image, painted mask and exported media aren't uploaded.
1. Input: reject early, downscale locally
The importer accepts static PNG, JPEG and WebP up to 80 MiB and 32 megapixels. Animated PNG and animated WebP are rejected on purpose. Large images are resized locally into a working canvas, because a 30-megapixel camera original won't improve a 480 px GIF but will happily use up memory on a phone.
Lesson one for client-side media tools: your user's device is the server. Cap the working resolution at what the output can actually show.
2. The mask is a weight map, not a selection
A binary mask (these pixels move, those don't) creates a hard boundary between moving and still pixels, and that boundary shows up right away as a tear.
So the mask is graded. You paint with a brush, and stronger paint allows a stronger response. Paint, Erase, brush size, undo/redo, Select all and Invert are the whole toolbox. The guidance I give users follows from the maths:
- Paint the centre of the soft object strongly and feather toward the edge, so the displacement falls off smoothly.
- Leave a thin unpainted "anchor" where the subject meets something rigid, like a floor, a table edge or a text panel.
- Keep eyes, lettering and straight architecture out of the strongest area, because deformation is most visible on high-contrast straight lines.
Conceptually, the final displacement of a point is roughly (simulated motion) × (mask weight at that point). A weight of zero means the pixel is pinned.
3. Motion: springs with a few personality knobs
The motion is "soft-body-ish". The image is treated as a flexible mesh, and the painted region responds to an automatic push like a damped spring. That push has a strength, a speed (the period) and a direction.
Each preset is a parameter set. In the shipped code the parameters include input, stretch, bounce, damping, cohesion, and a gravity direction and amount. Some contrasts show how the "feel" comes from the numbers:
- Soft wobble uses moderate bounce with fairly high damping and cohesion, so it settles gently.
- Spring bounce turns bounce way up and lowers damping, so it overshoots and rebounds.
- Quick shake uses low damping and low cohesion, so it gets jittery fast. That's also why it exposes rough mask edges first.
- Side sway drops gravity entirely, so the motion reads as a sideways sway rather than a sag.
Two practical takeaways if you build something similar:
- Expose presets, not physics. Most people don't want to tune damping. They want "bouncy" or "floaty". Strength, speed and direction cover most of the fine-tuning on top of that.
- Change one thing at a time. Strength and damping interact. If someone raises strength to compensate for a leaky mask, the background starts to swim. The fix is nearly always the mask, not the slider.
4. Rendering: WebGL2 for the live preview
The live preview needs WebGL2. Deforming a textured mesh and redrawing it at display rate is exactly what the GPU is good at. When the preview won't start, the troubleshooting advice is plain: update the browser, enable hardware acceleration and confirm WebGL2 works on that device.
Because the motion is deterministic (the same parameters give the same frames), what you preview is what gets recorded. That matters a lot for export. You don't have to screen-capture a live animation and hope the frame timing holds. You can step the simulation frame by frame at the target frame rate.
5. Export: probe what the device can actually encode
This is where browser support gets messy.
Video. The editor uses the WebCodecs VideoEncoder API and asks the runtime, via VideoEncoder.isConfigSupported(), which configurations it can really encode. It checks in order:
- AVC/H.264 → MP4
- VP9, then VP8 → WebM
- If neither works → GIF, which always has a software path
The browser name alone doesn't settle anything. OS codecs, hardware, browser build and settings all play a part, so the only trustworthy answer is to probe. For AVC, the dimensions get rounded to even numbers because the encoder requires it. Encoded chunks are muxed into an MP4 or WebM container with the open-source Mediabunny library. Export work runs in a Web Worker with OffscreenCanvas, so the UI thread stays responsive while frames are encoded.
GIF. GIFs are made with the small gifenc library, which quantizes each frame to a palette and applies it. GIF is the universal fallback, but it's indexed colour. Noisy photos, gradients and large moving areas make GIFs big fast. That's one more reason a focused mask matters.
Limits (longest side, aspect ratio preserved, never upscaled):
| Format | Frame rate | Max duration | Max dimension |
|---|---|---|---|
| MP4 (AVC) | 30 fps | 10 s encoder ceiling | 720 px |
| WebM (VP9/VP8) | 30 fps | 10 s encoder ceiling | 720 px |
| GIF | 16 fps | 5 s | 480 px |
The 10-second figure is an encoder validation ceiling. The current UI offers 2, 4 and 5 second buttons, so exports from the visible controls are at most 5 seconds.
One user-facing detail I'd recommend to anyone: if MP4 is missing, say why. A missing MP4 button means the browser doesn't expose a compatible local encoder. It isn't a sign that anything was uploaded or that the tool is broken. I also tell people that renaming .webm to .mp4 changes only the filename, not the container or codec.
6. Storage: local, and honest about its limits
Optional project history (source image, thumbnail, mask, motion settings) lives in IndexedDB, up to 20 recent projects depending on available storage. There's no cloud sync. Browser storage can be evicted, private windows forget everything, and clearing site data removes projects. So the UI nudges people to keep the downloaded file and the original image.
On privacy, being precise matters more than marketing. Image work never leaves the device, but the site itself has page analytics, which the privacy page discloses along with the consent handling. "No upload" is a claim about the image pipeline, and that's how I phrase it.
What I'd suggest if you build one
- Treat the mask as continuous weights from day one.
- Make motion deterministic so you can render export frames offline instead of capturing them live.
- Probe codecs with
isConfigSupported()and keep a GIF fallback. - Cap resolution early, and run encoding in a worker.
If you want to poke at the result, the demo cat is on the Purupuru Maker home page.
I'm Kangsen Ying (yksalun), and I build small single-purpose web tools. Purupuru Maker is free, needs no account, and processes images locally in the browser.












