Skip to content

Animation

Paint Forge ships a complete 2D animation system built on two complementary primitives: property keyframing (smooth tweens between snapshots of an object's numeric, color, or path-shape properties) and cel animation (discrete frame-by-frame sequences inside a frame-group layer). Both animation types share the same timeline, playhead, and export pipeline, so you can mix tween-style motion graphics with hand-drawn flipbook sequences in a single project.

Every feature on this page is driven by the Animation panel — a resizable dock at the bottom of the canvas — and persists alongside your project through save, load, and undo/redo.

Two mental models. Use property keyframing when you want a single object to move / scale / rotate / fade / change color over time. Use cel animation (frame groups) when you want to draw each frame by hand and swap between them at a fixed rate — think traditional animation or pixel-art character cycles.

Opening the Animation Panel

The Animation panel is a dockable bottom panel under the canvas — timelines are horizontal data, so full viewport width is the right shape. Two ways to open it:

  • Command palette (Ctrl+K) → "Open Animation Panel".
  • Menu bar → View → Open Animation Panel.

Once open, drag the top edge up or down to resize the dock. The close button (X) in the top-right collapses it — reopen anytime via the command above. Dragging the height below about 120px also auto-closes so you never get stuck with an unreadable strip.

Panel Anatomy

The Animation panel docked at the bottom of the editor. Transport bar on top, scrubber + zoom in the middle, track area below.

The panel is divided into three horizontal regions:

1. Transport Bar (top)

  • Duration slider — animation length from 500ms to 10,000ms.
  • FPS picker — 12, 24, 30, or 60 frames per second for playback.
  • Time scrubber — drag to seek through the animation; value shown in seconds.
  • Play / Pause button — transport control (Space also toggles when the panel has focus).
  • Add Keyframe — snapshots the active entity's properties at the current time.
  • Record Mode toggle — auto-creates keyframes every time you edit a watched property while the playhead is at a new time.
  • Graph — toggles the value-curve editor (see Graph Editor section below).
  • Preset dropdown — apply ready-made entrance / exit animations.
  • States dropdown — save / load / switch named timeline snapshots.
  • Timeline ops dropdown — reverse, scale time, or offset all keyframes at once.
  • Ghost keyframes / Motion trails / Snap / Active layer only — display + ergonomic toggles.
  • Audio button — attach an audio file to the timeline.
  • Detect beats — appears once audio is attached; energy-based onset detection drops a marker on each beat (see Beat Detection section below).
  • Export GIF / Lottie — quick-access in-panel exports. Use File → Export Animation for WebM / MP4 / APNG / sprite sheet / image sequence with motion blur.

2. Zoom Bar (middle)

A compact bar with zoom-out / zoom-in icons, a slider for pixels-per-second (10 – 1000), and a numeric readout. Ctrl/⌘+wheel over the track area zooms around the cursor so the time under your pointer stays stable.

3. Track Area (bottom)

Each animated entity in the scene gets one row. Rows come in three kinds:

  • Object row — one row per keyframed vector / text / raster object. Diamonds mark keyframe times.
  • Layer row — one row per layer that has layer-scoped keyframes (opacity, rotation, scale, offset) or adjustment-param keyframes.
  • Frame-group row — one row per frame-group layer. Shows cel blocks (live thumbnails of each frame) instead of keyframe diamonds.

Object and layer rows are expandable: click the chevron in the label column to reveal one sub-row per animated property so you can retime individual axes (e.g. just left, just opacity) independently.

Your First Animation

A 30-second walkthrough to tween a rectangle across the canvas:

  1. Press R and draw a rectangle on the canvas.
  2. Open the Animation panel (Ctrl+K → "Open Animation Panel", or View menu).
  3. With the rectangle still selected and the playhead at 0.00s, click Add Keyframe. A blue diamond appears on the track.
  4. Drag the Time slider to around 1.5s (or use . / , to jump between keyframes later).
  5. Drag the rectangle to a new position on the canvas.
  6. Click Add Keyframe again. A second diamond appears.
  7. Press Space or click Play — the rectangle tweens smoothly between the two keyframes.
What Add Keyframe captures. It snapshots every numeric property the active entity exposes — position, opacity, angle, scale, text size / spacing / line-height (on text), gradient stops (when fill is a gradient), and path data (when the object has a pathData field). Irrelevant properties are silently skipped by type.

Keyframable Properties

Anything in the tables below can carry independent keyframes. Keyframes are keyed by (entityId, property, time), so re-pressing Add Keyframe at the same time simply updates the existing snapshot rather than duplicating.

Object-scoped numeric

PropertyApplies toNotes
left, topAny objectScene-space position of the object center.
opacityAny object0 (transparent) → 1 (opaque).
angleAny objectRotation in degrees.
scaleX, scaleYAny objectScale factors. Independent axes.
strokeWidthAny object with a strokePixel width.
width, heightResizable objectsBounding-box dimensions.
fontSizeText / textbox / textonpathNumeric px.
letterSpacingText / textbox / textonpathPx added between glyphs.
lineHeightTextboxMultiplier on fontSize (1.3 = default).
fillGradientStop0Position … Stop7PositionObjects with gradient fillPosition (0–1) of each stop, capped at 8 stops.

Object-scoped color

PropertyApplies toNotes
fillAny object with a solid fillHex color; lerps in RGB space.
strokeAny object with a solid strokeSame as fill.
fillGradientStop0Color … Stop7ColorObjects with gradient fillColor of each stop, capped at 8 stops.

Path geometry

pathData — for path and textonpath objects. Two keyframes on the same object with different path data get smoothly tweened by resampling both shapes to 128 arc-length points and lerping per-point (bezier curves flatten during the morph, but final keyframe shapes are preserved).

Layer-scoped

PropertyEffect
layerOpacityFades the entire layer (composited below its children).
layerRotationRotates the whole layer around the document center.
layerScaleX, layerScaleYPer-axis layer scale around the document center. Animate each independently for non-uniform stretch.
offsetX, offsetYTranslates the layer content independently of object positions.

Adjustment-layer params

Any scalar numeric field inside an adjustment layer's configuration (brightness, contrast, hue, saturation, temperature, tint, exposure, highlights, shadows, clarity, …) can be keyframed via the adj.<field> naming convention. Select the adjustment layer and click Add Keyframe — every numeric field in the current config is snapshotted at once.

Complex structured fields (curves, levels, color balance) are out of scope for v1 — their nested shape needs bespoke interpolation. Scalar fields cover the common case (fade in color grading, pulse brightness, etc.).

Easings

Every keyframe carries an easing that controls the curve from the previous keyframe to this one. Right-click a keyframe diamond → Edit Easing… opens the bezier curve editor.

Named presets

NameCurve
linearConstant velocity — no acceleration.
ease-inStarts slow, accelerates. Quadratic.
ease-outStarts fast, decelerates. Quadratic.
ease-in-outSlow at both ends, fast in the middle.
bounceEase-out with three decreasing bounces. Classic Penner curve.
springDampened spring with a slight overshoot, then settles.
step-beforeNo interpolation — jumps to the next value immediately after the prev keyframe.
step-afterNo interpolation — holds the prev value until the next keyframe time, then jumps.

Custom cubic-bezier

The Edit Easing… popover renders a unit-square canvas with two draggable handles — the same four-control-point model CSS uses. Drag P1 (bottom-left handle) and P2 (top-right handle) to shape the curve. Five preset buttons (Linear / Ease / Ease-In / Ease-Out / Ease-In-Out) snap to standard CSS curves. Apply commits the curve onto the keyframe; Cancel discards.

When to use step easings

Step easings are perfect for flipbook sequences, UI state flags, or boolean values you want to change discretely rather than smoothly.

Graph Editor

Click the Graph button in the transport bar to swap the horizontal track rows for a value-curve plot. X axis is time (matching the timeline's duration), Y axis is the keyframed property's numeric value. It's the animator's tool of choice for reading and shaping motion — straight-line diamonds on the track view hide the actual velocity curve; the graph makes it explicit.

Streams + selection

Each animated property is a stream. Every visible stream draws its own colored curve; the selected stream renders at full opacity + 1.5 px width, while other streams dim to 0.45 opacity. Hit-testing only engages on the selected stream, so overlapping curves never trip each other. Click a keyframe dot on the selected stream to select it; click the empty plot area to deselect.

Interactions

  • Drag a selected dot — simultaneously retimes (x axis) and revalues (y axis) the keyframe.
  • Drag a tangent handle — reshapes the easing curve arriving at the selected keyframe. The handle position maps to the cubic-bezier control point the keyframe carries.
  • Wheel — zooms the time axis around the cursor so the time under your pointer stays stable.
  • Ctrl / ⌘ + wheel — zooms the value axis instead (useful for overshoot curves or tight numeric ranges).
  • Shift + drag — pans the time axis.
  • Click empty plot — deselects the current keyframe.

Adjacent keyframes in each stream are connected by a 32-sample interpolation curve that respects the arriving keyframe's easing, so the line you see is the actual motion profile — not a straight segment. Value range auto-fits to the union of all visible streams with 10% padding so outliers don't hug the edge.

When to use the graph editor. Track view is best fortiming (when something happens); graph view is best for shape (how it happens). Shaping ease-in-out overshoot curves, matching velocity between two segments, or diagnosing why a tween feels "off" are all radically faster in graph view.
Graph view with no keyframes yet. Toggle the Graph button (highlighted blue) to swap between track view and value-curve view.

Record Mode (Auto-Keyframe)

Toggle the Record button in the transport bar to enter auto-keyframe mode. While record mode is active, every object-level transform commits a keyframe at the current playhead time automatically — no need to press Add Keyframe after each tweak. The scrub → drag → scrub → drag workflow replaces the older add-keyframe → scrub → add-keyframe → scrub loop.

  • Watched properties: left, top, opacity, angle, scaleX, scaleY. Fired from object:modified events so any tool that commits a transform triggers a capture (Select, drag, resize, rotate).
  • Re-editing at the same time updates the existing keyframe rather than duplicating.
  • Disabled during playback — would otherwise fight the playback-driven interpolation.
  • Requires non-zero timeline duration. Toasts + history commits are debounced by 400 ms so ten rapid drags show a single "Recorded N keyframes at T.TTs" summary.
  • Other animatable properties (fill color, text metrics, gradient stops, path data, adjustment params) still require manual Add Keyframe.

Wiggle / Noise Modifiers

Organic motion without hand-placing dozens of keyframes. Right-click a numeric keyframe → pick a strength:

  • Subtle — 2Hz, 3 unit amplitude. Good for idle jitter.
  • Normal — 2.5Hz, 10 unit amplitude. Generic natural motion.
  • Strong — 3.5Hz, 30 unit amplitude. Shaking, vibration effects.

Wiggle superimposes deterministic noise (sum of two sines at different frequencies) on the eased value within a segment. The noise is amplitude-damped with a quadratic envelope so it's exactly zero at both keyframe boundaries — no discontinuities where segments meet. Each preset seeds with a fresh random integer so re-clicking the same preset produces a different-looking shake.

Remove a wiggle by right-clicking the same keyframe → Remove Wiggle.

Expression-Valued Keyframes

Replace the interpolated value with a procedural formula. Right-click a numeric keyframe → Add Expression… opens a textarea with a live preview of the evaluated value at the current playhead.

Syntax

  • Operators: + − * / %, unary − and +, parentheses.
  • Numbers: integers, decimals, scientific notation (1e-3).
  • Identifiers: time (seconds since segment start), t (alias), duration, fps, PI, E.
  • Functions: sin, cos, tan, abs, sqrt, floor, ceil, round, exp, log, min, max, pow.

Examples: time * 50, 50 + sin(time * 2 * PI) * 30, max(0, 100 - time * 100).

Security

The expression evaluator is a hand-rolled recursive-descent parser with a hard-coded identifier whitelist — no eval or new Function. User formulas cannot access window, globalThis, process, or any other runtime global.

Motion Paths

Animate an object's position along a user-drawn vector path rather than a straight line between keyframes. Workflow:

  1. Draw a path object with the Pen tool (X).
  2. Select the object you want to animate along the path.
  3. Keyframe its left and top at two times (e.g. 0s and 2s).
  4. Right-click the later keyframe diamond → Attach Motion Path….
  5. Click the path on the canvas. The object now follows the path instead of lerping position between keyframes.

Motion paths require both the left and top keyframes at the same time to carry the same motionPathId — the attach dialog refuses if only one axis is keyframed.

Orient along path

After attaching, the context menu gains an Orient Along Path toggle. When on, the moving object's angle tracks the path's tangent direction — useful for rockets, fish, cars, or anything that should face where it's going.

Motion trail overlay

The Motion trails checkbox in the transport bar (default on) draws every referenced path as a dashed indigo polyline on the canvas overlay, with an arrowhead at the end. Useful even when the source path object is hidden.

Parent-Child Rigging

Animating a parent object moves every descendant with it — a character's torso drags head + arms along without extra keyframes.

  1. Right-click a child object on the canvas → Parent to….
  2. A purple banner appears at the top of the viewport; the cursor becomes a crosshair.
  3. Click the intended parent object on the canvas.
  4. The parent-child link is committed; animating the parent now moves the child.
  5. Right-click the child → Detach from Parent to unlink.

Cycles are refused automatically (A → B → A), as is self-parenting. The link survives project save / load.

Frame Groups (Cel Animation)

A frame group is a group layer flagged for cel animation. Its child layers become discrete frames — only one is visible at a time, and playback cycles through them at a configured frame rate.

Creating a frame group

  • Layer menu → Convert Group to Frame Group (promotes an existing group).
  • Select multiple layers → Layer menu → Group as Frame Group (one-shot).
  • Command palette → "Convert Group to Frame Group" or "Group as Frame Group".

Playback + per-frame controls

  • Frame rate — per-frame-group FPS (default 12). Independent of the global timeline FPS.
  • Loop mode — loop, pingpong, or once. Set in the Layers panel's expanded options.
  • Per-frame duration override — any child layer can set its own frameDuration (ms), breaking the fixed FPS for that one frame. Useful for held poses.
  • Alt + , / Alt + . — step backward / forward through frames with the keyboard.

Cel blocks on the timeline

A frame group's timeline row renders cel blocks — live thumbnails of each child frame, sized in proportion to that frame's playback duration. Click a block to jump the playhead to that frame; drag a block horizontally to reorder frames. The thumbnail regenerates via a debounced 5% downscale whenever the frame's content changes.

Draw-through + auto-advance

Two per-frame-group toggles in the Layers panel's expanded options:

  • Draw-through — when enabled, a paint stroke on any child frame is copied to ALL sibling frames on stroke end. Ideal for painting shared backgrounds across a whole animation cycle.
  • Auto-advance — steps currentFrameIndex forward and activates the next child frame automatically after every stroke. Keeps the animator in a single-input rhythm.

Both run BEFORE the history push, so a single Ctrl+Z rolls back the stroke, the sibling copies, AND the frame advance together. Covered tools: brush, vector brush, eraser, spray paint, calligraphy, smudge, blur, sharpen, dodge/burn, color replace, liquify, clone stamp, healing brush, red-eye.

Onion Skinning

Onion skin ghost-renders adjacent frames around the current one so the animator can see where they're coming from and going to. Paint Forge has two onion-skin systems:

Frame-group onion skin

Configured per frame group in the Layers panel. Fields: onionSkinEnabled, onionSkinOpacity, onionSkinPrevFrames / NextFrames (0–3 each), onionSkinPrevTint / NextTint. When enabled, ghosts of neighboring frames render around the active frame on the canvas. Applies to cel animation only.

Keyframed object onion skin

Covers property tweening. Toggle via the Ghost keyframes checkbox in the transport bar. When on, animated objects render at their previous and next keyframe times in tinted ghost form on the canvas overlay — red-ish for prev, green-ish for next. Pure view-layer; never mutates live object state.

Audio Track + Waveform

Attach an audio file to the timeline for scrub + playback sync.

  1. Click the Audio button in the transport bar.
  2. Pick an audio file — mp3 / wav / ogg / webm / m4a / aac / flac supported, up to 25 MB.
  3. A waveform strip renders below the ruler, sized to the timeline's content width.
  4. Play / pause / scrub now keeps audio synced with the playhead.

When signed in with a cloud project, audio uploads to Supabase Storage and the timeline stores the storage URL — keeps project JSON small. Without a cloud project, the audio is embedded as a base64 data URL in the project (fine for short clips, bloats big ones).

The waveform decodes once per clip via OfflineAudioContext and downsamples to 500 peaks — cached per source string so swapping the same clip back in doesn't re-decode.

Beat Detection

Once an audio track is attached, a Detect beats button appears in the transport bar. Click it and Paint Forge runs an energy-based onset detector on the decoded audio, dropping a coloured marker (sky-blue, id prefix marker-beat-) on every beat. Compounds beautifully with the keyframe marker-snap — drag a keyframe near a beat and it locks to the rhythm.

How it works

Dependency-free, tuned for percussive / drum-heavy music (music-video sync, rhythm games). The pipeline:

  1. Mix down channels to mono Float32.
  2. Rectify + window-sum into a coarse envelope at a 10 ms hop.
  3. Novelty curve = positive first derivative of the envelope — drops from loud to quiet don't count as beats; rises do.
  4. Adaptive threshold = running median over a ±500 ms window × the sensitivity multiplier. Local-maximum test inside a small neighbourhood prevents one transient from producing multiple adjacent beats.
  5. Enforce a minimum inter-beat gap derived from maxBpm (default 200 BPM → 300 ms).

Not FFT-based on purpose — FFT would sharpen detection on tonal / melodic content, but it adds an extra 200 lines or an external dependency. The simple approach clears the "close to industry quality for drums" bar.

UI flow

  1. Attach an audio file via the Audio button.
  2. Wait for the waveform to render under the ruler.
  3. Click Detect beats. While analysing, the button shows "Analysing…" and is disabled.
  4. If beat markers from a previous run already exist, a confirm dialog asks to replace them — manually-added markers are always preserved.
  5. On success, a toast reports "Detected N beats". Each beat marker is labelled Beat 1, Beat 2, …

Good audio for detection

Percussive music with clear transients (rock, hip-hop, electronic) yields excellent results. Slow orchestral pieces without drums may produce sparse or missing beats — an empty result is valid, not an error. You can always fall back to manually placing markers.

Timeline Markers

Named colored vertical lines on the ruler that flag important times — beat drops, scene boundaries, cue points.

  • Right-click on the ruler → "Add marker at N.NNs…" prompts for a name.
  • Right-click an existing marker → Rename / Change color / Delete.
  • M then a digit 1–9 jumps to the Nth marker (chord has a 500ms window).
  • Shift+, / Shift+. jump to the previous / next marker, mirroring the keyframe nav on , / .

Colors cycle through a 6-color palette automatically. Markers serialize alongside keyframes and survive save / reload.

Multi-Select + Copy/Paste

Click a keyframe diamond to single-select it. Shift-click (or Ctrl-click) to toggle additional keyframes in the selection.

  • Ctrl+C / Cmd+C — copy selected keyframes (all three types: numeric, color, path-data).
  • Ctrl+V / Cmd+V — paste at the current playhead with every time rebased so the earliest keyframe lands on the playhead. Pasted keyframes become the new selection.
  • Delete / Backspace — remove every selected keyframe in one step.
  • Escape — clear selection.

Drag-retime multi-select

Dragging a keyframe that's part of a multi-selection moves every selected diamond by the same delta with live group-drag visual preview. The earliest can't go below 0 and the latest can't exceed the timeline duration — if a move would push a keyframe out of range, the whole group's delta is clamped so the worst offender just touches the boundary.

Snapping

The Snap checkbox in the transport bar (default on) makes keyframe drags stick to two targets:

  • Frame boundaries — multiples of 1000/fps ms.
  • Nearby keyframes on the same track — within 6px of the draggable's center at current zoom.

Neighbor snap takes priority over frame snap takes priority over the raw time. Hold Shift during drag to disable snap temporarily regardless of the checkbox.

Global Timeline Ops

The Timeline ops dropdown operates on every keyframe, marker, and state at once:

  • Reverse — mirror every time across the duration. Animation plays backwards.
  • Scale Time… — prompt for a factor (0.1–100); multiplies every time, clamping to duration.
  • Offset… — prompt for milliseconds; shifts every time by that delta, pinning at 0 and duration.

All three commit a labeled history snapshot so Ctrl+Z rolls the entire transformation back in one step.

The Timeline ops dropdown — Reverse, Scale Time…, and Offset… each mutate every keyframe and marker at once with a single labelled history commit for clean undo.

Animation Presets

The Preset dropdown ships 9 ready-made keyframe recipes. Click one with an object selected at the current playhead, and the preset's keyframes are added in a single step:

PresetEffect
Fade InOpacity 0 → current, ease-out.
Fade OutOpacity current → 0, ease-in.
Slide UpEnter from one height below + fade in, ease-out.
Slide DownEnter from one height above + fade in.
Slide LeftEnter from one width right + fade in.
Slide RightEnter from one width left + fade in.
PopScale 0 → current with spring overshoot + fade.
Bounce InDrop from 2× height above with classic bounce easing + fade.
Spin InRotate from −360° + scale up + fade in.

Each preset snapshots the object's current transform and opacity as the target state — so moving the object and reapplying still animates to the right position. Duration defaults to 800ms, clamped to the remaining timeline length.

The Preset dropdown — each entry includes a short human-readable description of what the animation will look like. Scroll inside the dropdown to see all 9 presets.

Named Animation States

Save snapshots of the entire timeline under a name ("idle", "walk", "jump") and switch between them. Perfect for game / UI motion state-machine workflows.

  • States dropdown → "Save current state…" — prompts for a name; snapshots keyframes + markers + duration + fps.
  • Click a state's name in the dropdown to LOAD it (overwrites the live timeline).
  • Per-state Update button — overwrite the saved state with the current timeline.
  • Per-state Rename / Delete.
  • Active state is highlighted with a blue dot and shown inline next to the button.

The audio track is not part of state snapshots — it stays shared across states, matching the common pattern of one audio bed accompanying many poses.

Per-Layer Filter

Large projects can have dozens of animated entities. The Active layer only checkbox in the transport bar filters the track list to show just the entities on the currently-active layer — objects whose layerId matches, plus the layer itself and any frame groups that ARE the active layer. Toggles in real time as you switch layers.

Keyboard Shortcuts

All shortcuts fire when the Animation panel has keyboard focus (click anywhere inside it first). Most require no modifier — Shift for case-sensitive variants; clipboard shortcuts need Ctrl / Cmd.

ShortcutAction
SpacePlay / Pause transport.
Home / EndJump playhead to 0 / duration.
/ Step playhead one frame backward / forward.
KAdd keyframe for the active entity at the current time.
Shift + KDelete every keyframe on the active entity within ±1ms of the playhead.
, / .Jump to the previous / next distinct keyframe time.
Shift + , / Shift + .Jump to the previous / next marker.
M then 1–9Jump to marker N (500ms chord window).
Ctrl + C / Ctrl + VCopy / paste selected keyframes.
Del / BackspaceDelete selected keyframes.
EscClear keyframe selection (or cancel motion-path / rigging pick modes).
Alt + , / Alt + .Step through frames in the active frame group.

Undo / Redo Integration

Every timeline mutation — add / delete / retime keyframe, change easing, attach / detach motion path, toggle orient-along-path, add / remove wiggle, set expression, apply preset, save / load / overwrite / delete state, reverse / scale / offset timeline, add / rename / delete marker, load / clear audio — commits a labeled history snapshot so Ctrl + Z rolls them back alongside canvas edits.

Non-destructive operations (scrub, play / pause, selection, expansion toggle, display checkboxes) explicitly do NOT commit history — they don't change the source of truth.

Mobile Ergonomics

On touch devices (viewport width ≤ 767px portrait or ≤ 500px height landscape), the timeline auto-adapts:

  • Keyframe diamonds grow from 12×12px to 22×22px for comfortable tapping.
  • Track rows taller (40px vs 28px) so the label + diamond don't crowd each other.
  • Click-drag threshold widens to 6px so touch-tremor doesn't trigger a spurious drag.
  • Desktop layout kicks back in on wider viewports automatically.

Importing Animation

Three command-palette imports turn external animation files into an editable frame group you can retouch, rotoscope over, or re-export.

Animated GIF → frame group

Ctrl+K → Import Animated GIF… decodes every frame of a .gif file into its own drawing layer, groups them, and flips the group into a frame group. The source GIF's per-frame delays become per-layer frameDuration overrides so the first playback / re-export preserves the original timing exactly. After import you can paint directly on any frame, insert new frames, delete ones you don't want, or re-export in any supported format.

Video → frame group

Ctrl+K → Import Video… opens a dialog that samples MP4 / WebM / MOV files at a configurable FPS with optional start / end trimming. Each sampled frame becomes a child layer inside a new frame group — the foundation of rotoscope and video-reference workflows. Sampling uses an offscreen <video> element plusdrawImage; no server round-trip, no FFmpeg required.

Pair with the Rotoscope project type. File → New Project → Rotoscope starts you with the animation panel + reference panel both open and the brush selected by default — optimised for painting over imported frames.

Lottie JSON → static geometry

Ctrl+K → Import Lottie JSON… ingests shapes (rect, ellipse, polygon, star, line, path) and image layers from a Bodymovin / Lottie JSON file, preserving solid and gradient fills. Useful for starting from a designer-delivered motion mockup and repainting / re-animating it inside Paint Forge. Keyframe playback import is future work — only the static frame at time 0 is imported today.

Exporting Your Animation

Six formats from the unified Animation Export dialog (File → Export Animation), plus Lottie JSON and CSS @keyframes via their own paths. All formats render through the full timeline — interpolated keyframes, frame-group cels, motion paths, parent-child rigging.

Unified Export dialog

FormatBest forCodec / output
GIFUniversal web previews + social256 colours/frame via gifenc. Max 100 frames, max dimension 2048 px.
WebMLong clips, smallest filesVP8/VP9 via MediaRecorder + canvas.captureStream(). Modern browsers only.
MP4Wide device compatibilityH.264 via browser-native encode; falls back to WebM with a .webm extension if the browser lacks MP4 encode.
APNGLossless alpha animationAnimated PNG with full alpha. Bigger than WebM/MP4 but pixel-perfect.
Sprite SheetGame engines / frame atlasSingle tiled PNG of all frames; metadata JSON attached.
Image SequenceVideo editor post-productionZIP of numbered PNG / JPEG files (one per frame). Drop-in for DaVinci, Premiere, After Effects.

The dialog lets you set output scale, quality (lossy formats), frame delay / FPS, loop count (infinite / once / custom), which layers participate, whether the canvas background is baked in or kept transparent, and — importantly — motion blur parameters.

Motion blur

Every format except Image Sequence supports over-sampled motion blur:

  • Samples — 1 (off), 2, 4, or 8 renders per final frame. Sub-frames are averaged in linear light. More samples = smoother blur + proportionally longer export time.
  • Shutter angle — 0° to 360°, default 180°. Controls how much of each frame interval the shutter is open.
  • Shutter window math: shutter_ms = (angle / 360) × frame_delay_ms. At 24 fps + 180° that's ~20.8 ms of per-frame integration, matching the cinematic standard.
  • Scope — only keyframed motion gets blurred. Frame-group cels snap to their thumbnails because blurring across hand-drawn frames would smear them.
  • Use case — turn on 4× samples @ 180° for any timeline where objects move fast enough to feel strobed at native frame rate.

Lottie JSON

The Lottie button in the transport bar emits a Bodymovin-compatible JSON file (schema v5.7.1) that plays in any Lottie runtime (web, iOS, Android, React Native). Supported object types: rect, ellipse, polygon, star, line, path, textonpath, image. Supported animated properties: position (left + top merged into center-anchored p), rotation, scale (percent), opacity (percent), fill color (solid + gradient). Unsupported surfaces (text objects, effects, patterns, frame groups, motion paths, audio, adjustment layers, path-data morphing) emit a toast warning listing what was skipped; everything else exports cleanly.

CSS @keyframes

Ctrl+K → Export as CSS @keyframes… emits a pure CSS stylesheet — one @keyframes block per animated object, plus a .pf-obj-* class selector that applies it. No runtime, no JSON, zero dependencies. Supports transforms (translate, rotate, scale), opacity, and solid-colour background-colour. Per-segment easings become animation-timing-function entries per step. Unsupported properties (text-internal, gradient stops, path morphing, effects) are silently skipped with a toast.

Pick the right format. GIF for universal sharing; WebM / MP4 for anything over a few seconds; APNG when you need alpha without video; Sprite sheet for game engines; Image sequence for post-production video editors; Lottie for web / mobile vector motion; CSS @keyframes for runtime-free web hand-off.

Performance Notes

  • getStateAt / getColorStateAt / getPathDataStateAt memoize per (time, mutationVersion). applyStateAt calls all three back-to-back with the same time, so the second + third calls return cached results instantly.
  • Interpolation groups keyframes by objectId+property once per mutation and caches. Adding a keyframe invalidates the cache; scrubbing does not.
  • Frame-group cel thumbnails regenerate via a debounced 5% downscale when contentGeneration changes; they never render full-resolution.
  • Path sampling (for motion paths + path morphing) caches resampled arrays by pathData string — swap the same path back in, no resample.
  • Expression compilation caches parsed ASTs by source string. Parse failures are also cached (negative caching) so broken expressions don't re-parse every frame.

Troubleshooting

The object isn't animating during playback

Verify the object has at least two keyframes on the property you want to animate (a single keyframe is a static value). Check the timeline row — if the diamonds are all at the same time, the playhead never sees interpolation. Also confirm the playhead is actually moving; if isPlaying is off, Play is disabled.

"Keyframe both left and top at this time"

Motion paths need matching left and top keyframes at the same time. Add the sibling keyframe first, then retry Attach Motion Path.

Exported GIF is huge

GIF per-frame palette quantization adds ~256 colors × 3 bytes of palette overhead per frame. For long animations, use the Scale option in the GIF export dialog (0.5× halves pixel dimensions) or switch to WebM for video-compressed output — usually 5–20× smaller.

Audio drifts out of sync during playback

The panel corrects drift over 30ms automatically. Persistent drift usually means the audio is encoded at a different sample rate than the browser's playback path. Try re-encoding to 44.1 kHz MP3 or use the Audio Offset field to compensate.

Project save / load preserves animation state

All animation state — keyframes, markers, audio, saved states, motion-path links, rigging parents, wiggles, expressions — round-trips through Serializer.metadata.timeline, so the full animation surface stays attached to the project across sessions.
Was this page helpful?
Animation - Paint Forge Docs