DEV Community

天知道997 997
天知道997 997

Posted on

Building Purupuru Maker: A Browser-Based Image Wobble Tool with React, Canvas, and Three.js

Purupuru Maker is a lightweight browser tool for turning still images into soft, jelly-like wobble animations. The core idea is simple for the user: upload an image, paint the area that should move, tune the motion, and export the animation. Under the hood, the project combines React, TypeScript, Canvas 2D, Three.js, and browser-native media APIs to create an animation workflow that runs locally without sending user images to a server.

Why This Project Is Built as a Client-Side Tool

Image animation tools often feel heavier than they need to be. Many require installing desktop software, uploading artwork to a remote service, or learning a timeline-based editor before producing a simple animated sticker or avatar. Purupuru Maker takes the opposite approach: the entire editing and export pipeline runs in the browser.

That constraint shapes the technical design. The app needs to decode images, let users paint a motion mask, preview a real-time deformation effect, and export media using APIs that are widely available in modern browsers. It also needs to stay responsive while handling images up to practical web sizes. React manages the application interface, Canvas 2D handles direct mask editing, Three.js renders the deformable image mesh, and MediaRecorder or GIF encoding turns the preview into a downloadable file.

The High-Level Architecture

The application is built with Vite, React, and TypeScript. Vite keeps local development fast, while TypeScript is especially useful for the shared data structures that move through the editor: loaded image metadata, motion settings, mask state, export settings, and deformable mesh vertices.

The main workflow is organized around a few responsibilities:

  • Image loading normalizes the uploaded file into a browser-friendly bitmap.

  • Mask painting stores the user's brush strokes in a hidden canvas.

  • Mesh generation creates a grid of vertices over the image.

  • Mask sampling converts painted pixels into per-vertex motion weights.

  • Physics updates move the weighted vertices every animation frame.

  • Three.js renders the image texture on the deformed grid.

  • Export modules record or encode the rendered canvas locally.

This separation keeps the product easy to extend. For example, adding a new motion preset mostly means changing motion settings, while adding a new export format can live in the export layer without rewriting the editor.

Turning an Image into a Deformable Mesh

The wobble effect is based on a textured grid. Instead of drawing the uploaded image as a flat rectangle, the renderer maps it onto a plane made of many small triangles. Each vertex in the grid stores its original position, current position, UV coordinates, motion weight, velocity, and offset.

On desktop, the mesh uses a denser grid than on smaller screens, which helps balance visual quality and performance. The grid is converted into triangle indices, then passed to Three.js as a BufferGeometry. The uploaded image becomes a CanvasTexture, and a simple MeshBasicMaterial displays that texture without lighting complexity.

This approach is efficient because the browser's GPU handles most of the textured rendering work. Each frame only needs to update vertex positions, mark the position buffer as dirty, and let Three.js draw the result.

Painting Motion with a Canvas Mask

The user does not directly edit the image. Instead, Purupuru Maker keeps the original artwork intact and stores brush strokes in a separate motion mask. Canvas 2D is a strong fit for this job because it supports fast pointer-based drawing, erasing, alpha blending, and pixel reads.

When the user paints, the mask records where motion should happen and how strong that motion should be. A visible overlay can show the painted area during editing, but the underlying image remains unchanged. This makes the editor non-destructive and keeps the mental model simple: paint the parts that should wobble, erase the parts that should stay still.

Before previewing, the app samples the mask around every mesh vertex. The sampled mask value becomes the vertex's motion weight. A small edge fade reduces movement near image borders, and several smoothing passes blend neighboring weights so the deformation feels organic instead of jagged.

The Wobble Physics Model

The animation is not a single sine wave applied uniformly to the image. Each weighted vertex moves toward a procedural target, then a spring-damping model decides how it follows that target over time.

The target motion is generated from a mix of sine and cosine functions using the vertex's normalized position, the current time, motion speed, strength, direction, and randomness. That creates subtle spatial variation, so the image feels soft and lively rather than mechanically sliding as one flat piece.

The spring system gives the motion its character. A higher spring value makes vertices chase their target more eagerly. Damping controls how quickly motion loses energy. Strength determines the scale of the wobble, while clamping keeps offsets bounded so extreme settings do not tear the image apart.

The app also supports motion envelopes, including looping motion and settling motion. A settling envelope can start strong and decay over a short duration, which is useful for bounce-style effects that feel like a quick reaction instead of an endless loop.

Rendering with Three.js

Three.js is used as a focused rendering layer rather than a full 3D scene engine. The scene contains an orthographic camera and a single textured mesh. The orthographic camera maps the image coordinate system directly into the renderer, which makes it easier to align pointer input, mask sampling, and export dimensions.

Every animation frame follows a compact loop:

  1. Update mesh physics based on elapsed time and motion settings.

  2. Write the new vertex positions into the geometry buffer.

  3. Render the scene to the WebGL canvas.

Because the renderer preserves the drawing buffer, the same canvas can be used for preview and export capture. That keeps exported media visually close to what the user sees in the editor.

Local Export: MP4 and GIF

Export is intentionally local. For MP4-capable browsers, the app uses HTMLCanvasElement.captureStream() with MediaRecorder. The renderer's canvas becomes a media stream, MediaRecorder collects video chunks, and the final Blob is downloaded directly by the browser.

GIF export follows a different path. The app captures frames from the rendered canvas, reads pixel data, quantizes colors with gifenc, writes frames into a GIF encoder, and returns a downloadable GIF Blob. This is more CPU-intensive than MediaRecorder, but it gives users a format that remains useful for stickers, posts, and lightweight sharing.

Both export paths report progress and support cancellation. That matters because media generation can take a few seconds, especially for longer animations or larger images.

Privacy as a Technical Feature

One of the most important design choices is that uploaded images stay local. The app does not need a server-side rendering pipeline to create the effect. Image decoding, mask editing, mesh deformation, preview rendering, and export all happen in the user's browser.

This is not only a privacy benefit; it also simplifies infrastructure. There is no upload queue, no storage lifecycle, no server-side image processing cost, and no need to retain user-generated artwork. For a creative tool where users may test personal drawings, avatars, fan art, or unreleased assets, that local-first model is part of the product experience.

Performance Lessons

The project highlights a few practical browser graphics lessons:

Dense meshes look smoother, but every extra vertex adds per-frame work. Mask sampling should happen when the mask changes, not every frame. Physics should use a stable delta time so animation does not explode after a tab pause or frame drop. Large images should be downscaled before editing so the tool remains fast on ordinary laptops.

The architecture also benefits from using the right rendering mode for each task. Canvas 2D is excellent for brush input and pixel masks. WebGL, through Three.js, is better for repeatedly drawing a textured deformable mesh. Keeping those jobs separate makes the app simpler and faster than forcing one API to do everything.

What Comes Next

Purupuru Maker's current architecture leaves room for richer creative controls. Future improvements could include more motion presets, undo and redo for mask edits, project save and restore, finer pinning or freeze masks, better mobile gestures, and additional export options through newer browser APIs such as WebCodecs.

The underlying pattern is flexible: a user-authored mask becomes weights, weights drive mesh motion, and the browser renders the result in real time. That pattern can support more than a jelly wobble. With different physics, presets, and masks, the same foundation could power breathing animations, subtle idle loops, heartbeat effects, shake reactions, and playful sticker motion.

Purupuru Maker shows how far modern browser APIs can go for small creative tools. With a focused architecture and a local-first pipeline, a static image can become an animated, shareable moment without leaving the page.

Top comments (0)