# Keyframe.it > Keyframe.it is a free, browser-based 2D skeletal animation editor for game-ready sprites. Rig artwork with bones, deform it with weighted meshes, animate on a keyframe timeline, and export straight to pixi-spine — no install required. It is especially suited to animating slot-machine and game symbols. ## What it is Keyframe.it runs entirely in a modern web browser. It is aimed at game developers, slot/casino-game artists, and 2D animators who want a fast, free alternative for rigging and animating 2D sprites and exporting them to game engines via the Spine runtime (pixi-spine). ## Key features - Bone rigging with forward kinematics (FK) and inverse kinematics (IK) - Mesh deformation with bone weighting (skinning) for natural bending and squash & stretch - Spring physics for automatic secondary motion (tails, hair, cloth, ears) - Keyframe timeline with a curve/graph editor and dope sheet - AI-assisted rigging from an image, and AI-assisted animation from a plain-text description - One-click slot-symbol animation pack: generate idle, win, drop, and destroy clips at once - Import and export Spine bundles (.json + .atlas + .png) compatible with pixi-spine - Additional exports: GIF, sprite sheet, and video - PSD import (each layer becomes an animatable part) - Built-in image editor: layers with blend modes, full selection toolkit (lasso, polygon, rect/ellipse, custom shape, magic wand, select brush), brushes, bezier Pen paths, editable Text layers (custom .ttf/.otf fonts, thick outlines, 13 Photoshop-style warps, vertical text, distortion), gradients, clone, effects - Sheet slicer that cuts parts with boxes, ovals, freehand lasso, or a one-click magic wand (Shift adds regions to a part, Alt carves them out) ## Pricing Free to use in the browser. Optional AI features use the user's own Anthropic API key (billed to them); donations are optional. ## For AI agents driving the editor The stage is a canvas — there is no DOM to read, and dragging pixels is slow and error-prone. Instead use the scripting API exposed on `window.keyframe` (available on every page load, no setup): - `window.keyframe.help()` — one line per method; start here - `window.keyframe.describe()` — mode, active animation, playhead, counts - `window.keyframe.getState()` — the whole document as JSON: bones (with parents and setup transforms), slots, animations with their keyed bones - `window.keyframe.getBone(name)` / `getWorldTransforms()` — numeric world positions and rotations at the current playhead. Verify poses with numbers, not screenshots. - `window.keyframe.poseBone(name, {x?, y?, rotation?, scaleX?, scaleY?})` — set bone values by name. Setup mode edits the rig; animate mode KEYS the pose at the playhead. - `applyPose({bone: patch, ...}, {time?})` — set MANY bones in one call (one undo step); `time` moves the playhead first. One call per animation frame. - `await renderFrame({time?, animation?, size?})` — a PNG data URL of the posed character, no UI chrome. LOOK at your pose after setting it, then adjust. - `await renderClip(animName?, {fps?, size?})` — an animated-GIF data URL of a whole clip, to judge the motion itself. - `checkpoint(label?)` / `listCheckpoints()` / `revertTo(idOrLabel)` — snapshot the document before an experiment and roll back cleanly if it didn't work. - `window.keyframe.keyBone(name)`, `deleteKey(name, time)`, `setPlayhead(seconds)`, `setMode('setup'|'animate')`, `setActiveAnimation(name)`, `createAnimation(name)`, `play()`, `pause()`, `undo()`, `redo()`, `selectBone(name)` - `getKeys(animName?)` — EVERY keyframe in a clip: per-bone transforms PLUS per-part `slotTracks` (colour keys incl. alpha, image swaps, mesh deform), draw-order and events. If a part is mysteriously invisible at some time, look here FIRST — a stray alpha-0 colour key shows up plainly in `slotTracks[part].color`. - `tintPart(partName, {r,g,b,a}|null, {time?, easing?})` — RGBA multiply tint (each 0..1). `easing:'step'` holds the colour until the next key (blinking lights). Setup mode sets the static tint (null clears); animate mode KEYS it at the playhead (fades, hit-flashes, glow pulses). `deleteColorKey(partName, time)` removes one colour key; `clearColorTrack(partName)` wipes the part's whole tint track from the active clip — the rescue when stray colour keys are hiding a part. - Rig editing: `addBone(parentName, {name?, x?, y?, rotation?, length?})`, `splitBone(name, t?)` (insert a joint mid-bone), `setBoneEndpoints(name, {start?, end?})` (reposition by WORLD start/end points — children keep their placement), `reparentBone(name, newParent)`, `renameBone`, `setBoneLength`, `deleteBone(name)` (removes the bone AND its child bones; parts always survive, reattaching to the parent bone with their world placement kept) - `screenFromWorld(x, y)` / `worldFromScreen(clientX, clientY)` — convert between world and viewport coordinates for the rare click the API doesn't cover. (In the UI, Alt-dragging a bone's joint/tip in setup mode moves just that endpoint too.) - Meshes: `await autoMesh(slotNames?)` traces deform meshes for parts, then `bindWeights(slotNames?)` binds them to nearby bones — parts bend at joints instead of staying rigid. Do this after rigging, before animating. - IK: `listIk()`, `addIk(boneName)`, `setIkTarget(name, x, y)` (world coords — setup edits the rig, animate KEYS it at the playhead), `setIkMix(name, 0..1)`. Plant feet / aim hands with IK targets instead of solving FK angles. - Curves: `setKeyEasing(bone, time, easing, bezier?)` — vary easing deliberately; uniform easeInOut everywhere reads as floaty. - Springs & wind: `setSpring(boneName, {stiffness?, damping?, gravity?, wind?, motion?} | null)` puts sway physics on a bone — automatic secondary motion for tails, hair, cloth, rising smoke (springs bake to FK keys on Spine export); `setWind({strength?, speed?})` drives all spring bones globally. - Draw & blend: `reorderPart(partName, -1|1)` steps a part back/front in draw order (animate mode keys it); `setBlend(partName, 'normal'|'additive'|'multiply'| 'screen')` — additive/screen for glows and smoke. - Game markers: `addPoint(boneName, {x, y, rotation?, name?})` / `movePoint` / `deletePoint` / `listPoints()` — point attachments in WORLD coords (muzzle flashes, emitters). `addBox(boneName, {points?, name?})` / `deleteBox` / `listBoxes()` — bounding-box hit areas (WORLD polygons). Both export to Spine. - Clipping: `setClip(partName, worldPoints[], {endPart?, inverse?})` turns a part into a mask over the parts drawn after it (windows, portals, iris wipes); `clearClip(partName)` removes it. - Events: `addEvent(name, time, {string?, int?, float?, audio?, volume?, balance?}?)` / `deleteAnimEvent(name, time?)` — named timeline markers with payloads, exported for game runtimes (footsteps, sfx cues). - Skins: `listSkins()` / `setSkin(name|null)` — per-part image override sets (outfits, or the characters of an imported multi-skin rig like goblins). renderFrame/renderClip and exports honor the active skin. - Win FX: `await listFx()` catalogs the one-click effect packs (win glow, light rays, sparkle & star bursts, shockwave/pulse rings, coin shower, confetti, payline streak, electric arcs, orbiting sparkles, shine sweep, hit flash, pop, anticipation). `await addFx(id, {count?, intensity?, tint?, strength?})` keys one into the `win` clip (idempotent — re-apply to retune); `await removeFx(id)` removes just that pack. Apply these as the FINISHER after the motion is done. - NOT scriptable yet (ask the human instead of clicking): path constraints, transform constraints, sliders, and the graph editor's bezier handles beyond setKeyEasing. - `await validate()` — the editor's Problems lint as JSON. Run it before saying you're done, and fix what it reports. - `swapImage(partName, imageName|null, {fade?, time?})` — swap which image a part shows. Setup mode = permanent; animate mode KEYS the swap at the playhead, and `fade` (seconds) crossfades it in — 0 for blinks, ~0.15 to melt mood changes. (Fades against NOTHING — null→image or image→null, e.g. blinks — export to Spine as baked slot-alpha keys, so they fade there too. A crossfade between two real images still SNAPS in a Spine export: the format has no per-attachment alpha.) See `recipes('expression-swaps')` for the full face workflow. - Assets: `listAssets()`; `getAssetPng(name, maxSize?)` returns the art as a PNG data URL so you can LOOK at it directly; `cleanAsset(name, {removeBg?, tolerance?, cleanEdges?, trim?})` removes solid backgrounds (flood from the corners, or pass seed points), defringes halos and crops margins; `detectSprites(name)` previews sprite bounding boxes in a sheet; `sliceAsset(name, {rects?, addToScene?})` cuts a sheet into individual assets (auto-detects sprites when rects are omitted); `addAsset(name, dataUrl)`; `duplicateAsset(name)` copies an asset so you can experiment on the copy; `openImageEditor(name)` hands control to the human editor. Text labels ("WILD", "BONUS", …): render them yourself to an offscreen canvas (fillText/strokeText) and `addAsset(name, canvas.toDataURL())` — or suggest the human use the image editor's Text tool (editable text layers with custom fonts, thick outlines, and 13 warp styles like Arc/Bulge/Twist). Don't try to drive the editor's text UI yourself. Typical sheet workflow: `getAssetPng(sheet)` to see it → `detectSprites(sheet)` → adjust/confirm rects → `sliceAsset(sheet, {addToScene: true})` → `cleanAsset(...)` per piece if edges look fringed. (`sliceAsset` cuts rectangles only; the human Slice-sheet UI can also cut oval/lasso/magic-wand-shaped parts — suggest it if a rect crop would drag in surrounding art.) - `await window.keyframe.loadExample('noir')` — load a demo rig ('noir' is a full production character: weighted meshes, springs, smoke clips; also 'monkey', 'cat', and slot symbols); resolves when the doc is actually in (returns bone/part counts, safe to getState() right after) - `await window.keyframe.exportSpineJson('4.2')` — the document as Spine JSON, to check work end-to-end WORK FAST — round-trips and tokens are your budget: - BATCH: run several API calls in ONE JavaScript evaluation and return a single compact summary object. Ten separate evaluations for ten poses is the slow way; one evaluation that poses, samples `getWorldTransforms()`, and returns just the numbers you need is the fast way. - Return SUMMARIES, never dumps. `getState()` once at the start to learn names; after that use the cheap probes: `describe()` (counts + mode), `getBone(name)`, `getWorldTransforms()`. Don't echo whole documents or data URLs into your context. - Render SMALL while iterating: `renderFrame({size: 160})` / `renderClip(name, {size: 160, fps: 8})` — a data URL scales with size², and a 512px PNG can be hundreds of KB of base64. Render big once, at the end, as the deliverable. - Skip renders when numbers suffice: pose checks are `getWorldTransforms()` comparisons; only LOOK when judging art or motion quality. - Recipes exist for: slice-and-clean, expression-swaps, rig-character, mesh-and-weights, idle-animation, walk-cycle — `recipes(name)` before starting any of these beats discovering the order of operations by trial and error. Mutations return `{ ok: true }` or `{ ok: false, error }`; errors name valid alternatives. Methods take bone/animation NAMES (from `getState()`), never internal ids. API edits are undoable and update the UI live. Typical animation loop: `getState()` → `setMode('animate')` → `checkpoint()` → `applyPose({...}, {time: t})` per frame → `renderClip()` to judge the motion → adjust or `revertTo()` → `exportSpineJson()` when happy. BEFORE starting a multi-step task, call `window.keyframe.recipes()` and read the matching recipe (`recipes('walk-cycle')` etc.) — they encode the right ORDER of operations for slicing sheets, rigging, meshing/weights, idle loops and walks. `attachSlot(partName, boneName)` attaches parts to bones (world position kept). SAVING: the project AUTOSAVES to the browser continuously — there is no save button you need to press. (The "+ save layout" button in the top bar saves the PANEL arrangement, not the project.) Deliverables leave through the API: exportSpineJson(), renderClip(), renderFrame() — or the human uses the Export menu. RIG & ANIMATION CRAFT (what separates alive results from stiff ones): - LOOK before you rig: `getAssetPng()` / `renderFrame()` + `getState()` first, and identify what the object IS — creature, logo, vehicle, slot symbol. Design a rig for THIS object, not a humanoid template: bones at its natural pivots, named for what they control (`ear_l_base`, `hair_curl_tip`, `jaw`, `wheel_front`). - Follow-through comes from CHAINS: give floppy parts a base→tip bone pair (or `splitBone`) and key the tip lagging the base by 1-2 frames — or skip the keys and `setSpring` the tip. Tips that move in lockstep with bases look like cardboard. - Mesh ONLY what must bend (`autoMesh` + `bindWeights` on trunks, tails, torsos). Ears, cheeks, whiskers, letters, coins: a pivoted bone reads just as well and exports lighter. Rigid part = plain region = crisp. - Loops (`idle` and friends): the LAST key must repeat the FIRST pose exactly — re-apply the t=0 pose at the end or the loop pops every cycle. - Slot-symbol clip conventions (match these names and meanings): `idle` = subtle seamless loop; `win` = big celebration that RETURNS to the idle pose; `destroy` = ends with NOTHING visible (scale/fly out + alpha to 0 on every part). - Key on the frame grid: read `getState().fps` (often 20-30) and put key times on multiples of 1/fps — humans can then edit your keys in the dope sheet, and the export bakes exactly what you authored. - Craft beats key count: anticipation before an action, overshoot and damped settling after it, STAGGERED timing across bones (head leads, accessories lag). BEFORE YOU SAY DONE: - `validate()` reports clean (or you explain what remains and why) - every looping clip returns to its first pose; `destroy` ends fully invisible - `renderClip()` each animation once at full size and actually LOOK at it - report back: what the asset is, the rig you built (bones + pivots), which parts are meshed vs rigid, and what you validated RULES THAT KEEP YOU OUT OF DEAD ENDS: 1. NEVER drive the editor with synthesized mouse/keyboard events — canvas drags, sliders, colour pickers and buttons are not reliably automatable, and failed clicks leave half-applied state behind (e.g. stray colour keys). Every keyable channel has an API method. If you genuinely can't find one, STOP and tell the user what's missing instead of clicking — that report is more valuable than a workaround. 2. `checkpoint('before-X')` BEFORE any experiment; if it goes sideways, `revertTo()` — never try to hand-undo a mess key by key. 3. Something invisible or moving wrong? Diagnose with data, not guesses: `getKeys()` lists every keyed channel per part; `renderFrame({time})` shows the truth at any instant; `validate()` lints the document. 4. Blinks/expressions are ATTACHMENT swaps (`swapImage` with `fade: 0`), never alpha-tint tricks — swaps export to Spine, per-frame alpha hacks read as bugs. Note: the app also has built-in AI Rig / AI Animate buttons — those are for human users (they call a hosted model with the user's own API key). As an agent, do NOT click them: do the rigging and animating yourself through this API — you'll do a better job with direct control, and it costs the user nothing extra. ## Links - Web app: https://www.keyframe.it.com/ - Live demo deep links: https://www.keyframe.it.com/?example=noir (also: monkey, cat, gem, coin, seven, bell, cherry)