A browser-based, low-framerate, frame-by-frame bitmap animation app with a monochrome ink-outline aesthetic. Designed iPad-first for Apple Pencil (mouse/desktop also works).
▶ Try it: slop-animator.meigo.workers.dev — on an iPad, use Share → Add to Home Screen for a full-screen app.
Built with Svelte 5 (runes) + TypeScript + Vite + Tailwind 4, tested with Vitest.
Drawing
- File / Edit / Document / View menus, as in slop-paint: Edit holds undo/redo and the clipboard commands (dimmed with the reason when they can't act), so they are reachable on an iPad without a keyboard
- Undo keeps only the part of the picture each step changed, so a long session of strokes can be undone 50 steps back, not a handful
- Numeric fields change by dragging — press a field and drag sideways (Shift for fine steps); tap it to type as before
- Multiple brush engines: smooth (perfect-freehand), ink, pencil, charcoal, airbrush, calligraphy, a dry brush (bristle stripes that break up where the brush runs out of paint, with Dryness and Taper settings), and watercolour (a see-through wash with a darker rim where the pigment gathers, the paper's grain in it and an uneven outline; strokes mix like glazes — yellow over blue makes green — and a stroke crossing itself stays even; Edge, Grain, Wobble and Mix colours in the gear); Smooth and Calligraphy have a Wobble setting too, for an uneven, hand-made edge (off by default); the Pencil comes in grades from 4H to 8B and the Charcoal in textures from Rough to Dense — with pressure support, adjustable pressure curves (the brush and the eraser each have their own), Stream (the line trails the pen on a string, the same at any pointer rate) and Smooth (averages the stroke's path with no lag, optionally keeping corners where you pause), optional ink pooling that swells the mark where the pen lingers, and separate brush/eraser settings
- Blend tool (
J) — blur, smudge or smear the drawing with the pen: it moves the layer's own paint and lays down no colour. Smear goes from a soft blur to dragging paint along, Strength sets how much each pass takes; Tip (soft, hard, grain, rough) and its own pressure curve are behind the gear - Colour palette for the brush and the fill: 24 swatches (greys, hues, skin and earth tones) plus a custom picker
- Fill tool with its own colour and opacity — the bucket no longer shares the brush's swatch, so outlines and flats stay separate; eyedropper, lasso selection with float/transform
- Soft fill edges: the bucket and Fill enclosed run the fill behind the line to its middle, fading as the line gets stronger, instead of stopping in a staircase with pale gaps — even on a grainy Pencil or Charcoal line (Soft, 0–8 px, default 1, sets how softly it ends there; 0 for a hard pixel edge); with Expand, whichever reaches further, the grown edge feathered. Gap closes small breaks in an outline for the bucket as well as for Fill enclosed
- Fill every area an outline encloses in one press, behind the strokes — the animator's white-under-black-outline, without redrawing each region by hand
- Transparent background support with checkerboard view and a paint-behind toggle
- Per-layer alpha lock (lock transparency): brushes paint only over what is already drawn, and the bucket recolours existing pixels — toggle it with the checkerboard button on the layer row
- iPad: finger pans, Pencil edits (canvas and timeline). Two-finger pinch zooms, pans, and rotates; lift snaps to 90° if you are within ~3°
Animation
- Frame-by-frame timeline with keyframes and holds, scrubbing, and playback
- Timeline markers — a short label on any frame for navigation or a to-do note, added from the
timeline bar or with
n. They get their own lane pinned under the ruler, shown once a marker exists. Tap to jump, tap again to rename or delete, drag to move; with the playhead on a marker the timeline bar's marker button deletes it instead of adding. Markers follow inserted and deleted frames and are saved with the project - Animated properties get their own timeline rows under the layer or group they belong to — collapse them away when you are drawing, open them when you are timing (a group's header chevron still hides members; the Spline chevron folds only its tracks)
- A layer's opacity can be animated too: key it at any frame for a fade, or set a segment to hold for a hard cut
- A group can be faded as one thing as well — member opacity × group opacity. The Group slider sits on the group header in the layer panel (even when collapsed); Animate from the group's own row on the timeline bar, same as Animate group transform
- Animate, add-key (current frame), easing, step, delete-key and stop live on the timeline bar and follow the selected row. Drawing frame tools hide while a property row is selected, so the two keying strips are never on screen together
- Playback and timeline tools share one bar above the ruler (play, In/Out, add/clear/delete frame on the left; onion, boil, and fps/length on the right)
- A play range set with In/Out shows as two thin markers running the full height of the timeline — drag either one to adjust it, tap one to jump there. It bounds playback and the PNG sequence / video exports
- A drawing key is created by drawing on a hold. The timeline bar adds a frame (a hold on every layer, same as growing the length), blanks the current key, or deletes a frame — not insert / duplicate / hold
- A hold continues until a blank key (◇). Running out of cells on a layer does not stop it — Clear is how a drawing ends
- Loop keys — a Moho-style loop key replays the drawings before it until the next key; shown as a teal back-arrow with ghosted repeats, drag the arrowhead to set the cycle
- Onion skins — step by frames or by keyframes (holds don't use up a ghost). With a play range set they stay inside it, and when looping they wrap across the seam — so a cycle's last drawing shows while you draw its first
- WebGL line boil (that hand-drawn wobble on held frames), with a step setting so the wobble holds for N frames — boil on twos or threes
- Layers with visual groups (collapse, visibility, lock, drag-reorder, double-tap a name to rename) — lock or hide a group and every member follows, without disturbing their own settings; locked and hidden layers are read-only everywhere
- Duplicate a layer, or a whole group — the copy of a group brings its drawing layers, the group's own transform and animation, and lands right above the original (reference layers stay in the original, since a copy would have to share the same video)
Transform & deform
- Free transform of a selection, and of a whole layer, reference or layer group — the Transform tool acts on whichever row is selected (transforms compose
group ∘ layer ∘ cellat render); side handles stretch one axis, corners keep proportions (toggleable), and Flip horizontal/vertical mirrors a layer, reference or group in place — animated ones included - Flip a selection horizontally or vertically from the floating selection bar (lifts it as a free-transform float, so you can keep adjusting before committing)
- A transform left open (a moved selection, a bent pose or deform) is never lost: merging, resizing, editing the timeline or exporting applies it first, and one undo takes it back. Undo while one is open cancels just the transform
- A layer's transform can be animated: keys at any frame, per-key easing (linear, hold, ease in/out), and a step setting so a move can land on 2s/3s like the drawings. Drag a key along its row to retime it. Starting the track is on the timeline bar, not the Transform tool
- A group's transform can be animated the same way, so a whole character rig moves as one thing over time while its layers keep their own animation
- Painting on a transformed layer shows its paintable edge as a hairline, so a scaled-down layer no longer cuts strokes off without warning
- Deform tool — FFD grid warp plus a rigid (MLS) mode
- Pose tool — silhouette triangulation + geodesic-weighted MLS with per-handle rotation/reach gizmos, for posing a character drawing without redrawing it
- Outline-only drawings pose as a body, not a thin web — space enclosed by the outline counts as part of the shape, with no change to the artwork
- Outline — turn a solid drawing into an outline of adjustable thickness (half-steps from 0.5px), with Wobble (the line wanders across the edge) and Variation (it swells and thins); preview live, then Apply as one undo step. With a selection active it outlines only inside it, leaving the rest of the drawing alone
Reference & audio
- Reference layers (image/video) with a transform gizmo, lockable to pin them in place; selecting one switches to the Transform tool, and selecting a drawing layer again switches back — images persist with the project; video is opt-in per clip and can play its own soundtrack
- A video reference is a draggable clip on the timeline (no filmstrip) — drag to place it in time; drag either edge to trim the source (the kept picture stays in sync); speed is set in the layer panel. Its clip is its (trimmed) footage span, so it no longer holds its last frame past the end of the video — those frames render empty
- An image reference can be trimmed to a range of frames — drag either edge of its timeline clip to set when it appears; untrimmed, it stays visible for the whole project
- Audio track: import, waveform, synced playback, scrub-while-you-drag, drag-to-offset, mute, trim either end of the clip from its lane handles, and muxed into the MP4/WebM export
- Clipboard image paste + rasterize to a drawing layer — and copying a selection now puts a PNG on the system clipboard too, so pixels travel both ways between apps
Files & export
- Project files as zip (JSON + PNG per key cell, plus embedded reference media), autosave to IndexedDB (it waits while you're drawing, so a save never lands mid-stroke), global preferences
- Restore autosave — besides the latest autosave, up to three older copies at least 5 minutes apart; File → Restore autosave… lists them and brings one back. If the drawings go blank behind your back (an iPad can drop a backgrounded app's image memory), autosave pauses instead of saving the blanks over your work, and the list opens. Restoring an older copy keeps the newer one in the list. The Document menu shows how much memory the drawings take (amber on iPad past 600 MB, with a one-time hint)
- A project name drives the save and export filenames
- Export dialog — pick a format (PNG sequence, PNG frame, PSD frame, MP4, WebM via mediabunny, animated GIF via gifenc), then set what applies to it: size (100% / 50% / 25% — PSD always exports at full size), frame range (all frames, the play In-Out range, or a typed custom span), video quality (MP4/WebM), and colour count (GIF: 64/128/256)
- Save to Files on iPad/iPhone — File → Save to Files… and every export open the share sheet, so a file goes to a folder you pick instead of piling up as numbered copies in Downloads (Download stays one tap away). In the Home Screen app, which can't download at all, Save opens the share sheet too
- Known issue — Chrome on iPad: after the on-screen keyboard closes (renaming a layer, a marker label), Chrome can leave the app shifted up with a blank band below, until you open a new tab. It's a Chrome bug the page can't reach. Use Safari, or the Home Screen app (it runs on Safari's engine even when added from Chrome) — neither has it
- PNG frame export — a single image, rendered exactly like that frame of the PNG sequence (line boil included, transparent when the background is)
- PSD frame export for paint-up in Photoshop — visible layers keep their names and their opacity stays live, groups arrive as real folders, transforms are baked into the pixels, and a merged composite ships alongside. Reference layers and line boil are left out, so a PSD looks cleaner than a PNG of the same frame when boil is on
- Animated GIF export — one global palette, held drawings encoded once (a shot on threes is about a third the size), any colour count from 2 to 256, an optional grayscale palette for ink work, and frame delays that keep the true duration (a 12fps GIF really does last five seconds), at the export dialog's usual 100% / 50% / 25% size
Keyboard
Spacetap = play/pause,Spacehold = pan the canvas ·←/→step a frame (Shift= 10) ·Home/Endfirst/last ·↑/↓change layer ·0fit view1— actual size (100%) ·nadd marker ·</>previous/next markerbbrush ·eeraser ·gfill ·sselect ·llasso ·jblend ·[/]size of the active stroke tool (brush, eraser or Blend) ·oonion ·⌘Z/⌘⇧Zundo/redo- In a numeric field:
↑/↓change it by one step (Shift= ten),Entercommits,Escaperestores - The view keys (
0,1) and the mouse wheel are ignored while the pen or mouse is down
npm install
npm run dev # Vite dev server on localhost
npm run dev:lan # HTTPS over LAN — for iPad testing (accept the self-signed cert once)Other scripts:
npm run build # svelte-check + tsc + vite build (0 errors, 0 warnings is the bar)
npm test # Vitest — pure-logic unit tests (1870); canvas/DOM code isn't node-testable
npm run test:ipad # Playwright WebKit iPad smoke check (first run: npx playwright install webkit)
npm run lint # ESLint (runes-aware + Tailwind class conflicts) — Prettier runs via pre-commit hook
npm run deploy # build, then wrangler deploy (Cloudflare Workers static assets)src/anim/— document model (projects, layers, cells, keyframe resolution), compositing/render, onion skinssrc/core/— brush engines, pressure curve, selection, fill, transform math, WebGL boil, triangulation/geodesic pose weightssrc/state/appState.svelte.ts— the global$statestore: all mutations, undo history, preferencessrc/lib/— Svelte UI components (canvas, toolbar, timeline, layer list, gizmos, dialogs)src/persist/— project file (zip), preferences (localStorage), autosave (IndexedDB)docs/superpowers/specs/&docs/superpowers/plans/— dated design specs and implementation plans for each feature
Group-transform apply (pixel flatten), custom easing curves, a camera with per-layer parallax
depth, and tiled copy-on-write cell storage for an expandable canvas. See CLAUDE.md for the
detailed state and deferred-work list.
![]() Meigo Kukk owner |
![]() Claude Anthropic · co-author |
Grok xAI · co-author |
Assisted by Claude (Anthropic) and Grok (xAI).
GitHub lists Claude from noreply@anthropic.com. Grok’s
noreply@x.ai is not tied to a GitHub user, so it does not appear in the
sidebar. Commits may include:
Co-Authored-By: Grok <noreply@x.ai>

