SKIP TO CONTENT
GW

W-03CASE FILE

Animos

A browser motion editor: 3D templates, real keyframes, direct manipulation, and video export — with no backend and nothing leaving the machine.

ROLEDesign & Engineering
YEAR2026
CATEGORYCreative tool
STATUSLIVE
STACKTypeScript · React Three Fiber · Zustand · IndexedDB · MediaRecorder
W-03 / LIVE SIMULATIONTIMELINE.SIM
01

Overview

Animos turns a screenshot or screen recording into a looping product showcase: pick one of twelve 3D templates, drop media into it, adjust a small set of opinionated controls or manipulate the scene directly, then export an MP4 or WebM. It runs entirely in the tab. There is no account, no database, and no upload — projects and media live in IndexedDB and never leave the machine.

02

Problem

Motion tools split into two unhelpful halves: heavyweight native apps with a week-long learning curve, and CSS keyframe generators that can't produce a 3D device mockup. The gap is a tool where direct manipulation and keyframes are the same system — where dragging a gizmo and typing a number and setting a keyframe all write to one animation model instead of three parallel ones.

03

One document, three projections

One document, three projectionsFIG.

Project document

scene graph + animation tracks, in one Zustand store

projected as

  • Preview stage

    R3F canvas, rAF clock, dpr 1–2

  • Timeline

    draggable keyframe markers per property

  • Export canvas

    same pipeline, dpr locked to 1

Preview and export share one evaluator, so they cannot disagree about what a frame looks like.

evaluateSceneAtTime clones the base scene and writes evaluated track values onto dotted property paths. Anything without a track keeps its static value, so a template only authors what actually moves.

04

Technical decisions

  1. D-01

    MediaRecorder + canvas.captureStream for export

    Export reuses the exact render pipeline the preview uses, parameterised to a fixed resolution with device-pixel-ratio pinned to 1 for deterministic frame size. What you previewed is what gets recorded, and progress moves through an explicit rendering → encoding → finalizing state machine with a working cancel path.

    INSTEAD OFWebCodecs or ffmpeg.wasm, which buy offline-speed encoding at the cost of a second rendering path that can drift from the preview. The tradeoff is real: an N-second clip takes about N seconds to export.

  2. D-02

    One entry point for every property edit

    applyPropertyEdit(project, {targetId, property, value, autoKey, currentTime}) is what the numeric fields, the opacity slider, the on-canvas gizmo, and arrow-key nudging all call. With Auto Key off it writes the static base value; with Auto Key on it upserts a keyframe at the playhead. There is no second animation system to keep in sync.

  3. D-03

    A three-call gesture protocol for drags

    beginLiveEdit() snapshots, liveMutate() updates without touching history, commitLiveEdit() pushes exactly one undo entry. A slider drag across 200 pixels is one Undo, not 200. Scrubbing the timeline bypasses history entirely.

  4. D-04

    Separate IndexedDB databases, not separate stores

    idb-keyval only creates an object store during that database's upgrade event, so reusing one database name for several stores silently keeps the first and loses the rest. Projects, asset metadata, and asset blobs each get their own database name. This surfaced as a NotFoundError during browser-driven testing and is now written down in the architecture doc.

  5. D-05

    Store asset blobs as ArrayBuffer, not Blob

    Structured-cloning a Blob into IndexedDB rejects with a bare null in WebKit — no message, no stack. Found during cross-browser testing; the fix is to persist { buffer, type } and reconstruct.

05

Things that turned out to be harder than they look

  • Aspect-ratio duplication is additive, not a constraint solver: adaptation is baseline(newRatio) + (current − baseline(oldRatio)) per property, with baselines always re-derived from the template rather than stored
  • Templates author against placeholder layer ids resolved at instantiation, because real ids are generated per project — this keeps template definitions declarative instead of imperative id-plumbing
  • The transform gizmo and React-controlled props would fight, so onObjectChange diffs per leaf property and writes only what actually moved
  • Audio is decoded at import, so an undecodable file fails when you add it rather than when you press play
  • The AI editing layer emits ordinary editor state — real keyframes, real easing, real automation — so a single Undo reverses an entire natural-language request and there is no hidden AI state to reconcile
06

Results

E2E tests
33
Green on Chromium, WebKit, and Firefox
Unit tests
59
Deterministic engines: keyframes, easing, ratio adaptation
Templates
12
Device, UI, and editorial categories
Backend services
0
No auth, no database, no analytics
07

Lesson

An editor is a state-management problem wearing a UI costume. Getting the document model right — one store, one evaluator, one edit entry point — made every feature after it cheaper, and made cross-browser bugs findable instead of mysterious.

BACK TO INDEX
NEXT SPECIMENEverworld