# CLAJ > CLAJ is a browser editor for animated presentations that are recorded to video: lyric videos, annotated commentary videos, promos. A project ("deck") is a list of independent scenes, each a fixed-size canvas (e.g. 1920×1080 or 1080×1920) with its own length, background, optional song and elements. CLAJ is fully manual: nothing moves unless keyframes say so, so an agent controls every detail. Live at https://claj.cashchat.me. Everything is client-side; a deck is one JSON file. Agents can produce a deck as JSON, build one through the MCP server, or drive the open editor through `window.claj`. ## Deck format - [JSON Schema](/schema/deck.v1.json): the contract (`schema: 1`). Shared definitions: `Scene`, `Element`, `Props`, `Fill`. - Times are **milliseconds**; positions and sizes are **canvas pixels**; `rotation` in degrees; `opacity` 0–1. - Elements: `text` (`text`), `image` (`src`), `video` (`url`: YouTube, Vimeo or a direct file, plus `playback` segments), `box` (`children`; nests at most 3 levels, clips its contents). The last element in a list is drawn on top. - Every element has `frames`. `frames[0]` is the origin (`time: 0`, its starting look). Later frames hold only the props that change from their `time` on; unset props carry over from earlier frames. A frame's `transition` (`enabled`, `duration` ms, `easing`: `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out` or `cubic-bezier(…)`) animates from its `time` to `time + duration`; disabled, it cuts. Transitions on one element must not overlap. - Fills (`fill`, `color`, scene `background`): `{kind:"color",value}`, `{kind:"gradient",layers:[{type:"linear"|"radial"|"conic",angle?,stops:[{color,pos}]}]}`, or `{kind:"image",src}`. A gradient `color` on text makes gradient text. - Shapes (`shape`): `{kind:"rect"}` with `radius:[tl,tr,br,bl]`, `{kind:"oblique",insets:[8 numbers, % of box]}`, `{kind:"polygon",points:[[x%,y%],…]}`. - URLs must be `https://` / `http://`, or `data:image|audio|font/…`. Uploaded files live in `assets` and are referenced as `{"asset": id}`. - Optional `scene.defaults`: per element type (`text`, `image`, `video`, `box`), Props a new element in that scene starts with (never position). Changing it doesn't alter existing elements. - Ids (scenes, elements, frames, segments) are short unique strings of your choosing. ## Load a deck - [Demo deck](/examples/demo.claj.json): a lyric slide and a portrait short; open it with `/?src=/examples/demo.claj.json`. - Give the user the JSON file to open with the editor's **Load** button, or - Link `/?src=`: the editor asks the user, then loads it. The host must allow cross-origin reads (CORS). ## MCP server `POST /mcp` (Streamable HTTP, stateless, JSON responses). Tools: `claj_new_deck`, `claj_add_element`, `claj_add_frame`, `claj_validate_deck`. Each takes the current deck and returns the updated one; nothing is stored on the server. ## In-page API (browser agents) With the editor open: `window.claj.getDeck()`, `loadDeck(deckOrJson)`, `scenes()`, `showScene(id)`, `addElement(type, {name,text,src,url,parentId,props})` → id, `seek(ms)`, `play()`, `pause()`, `time()`, and `record({tabAudio})` (must run inside a user gesture; otherwise click the Record button). Controls have accessible names (button titles) for UI automation. ## Examples ### Lyric line fading in on the beat ```json {"schema":1,"name":"Lyric demo","assets":{},"fonts":[],"scenes":[{"id":"s1","name":"Verse","width":1920,"height":1080,"duration":8000, "background":{"kind":"color","value":"#050b24"},"elements":[ {"id":"l1","name":"Line 1","type":"text","text":"Neon nights", "frames":[{"id":"f0","time":0,"props":{"x":360,"y":440,"width":1200,"height":200,"fontSize":140,"opacity":0, "color":{"kind":"gradient","layers":[{"type":"linear","angle":90,"stops":[{"color":"#2ec8ff","pos":0},{"color":"#b45cff","pos":100}]}]}}}, {"id":"f1","time":1200,"props":{"opacity":1},"transition":{"enabled":true,"duration":400,"easing":"ease-out"}}, {"id":"f2","time":3600,"props":{"opacity":0},"transition":{"enabled":true,"duration":300,"easing":"ease-in"}}]}]}]} ``` ### Commentary: video pauses while a callout appears ```json {"schema":1,"name":"Commentary demo","assets":{},"fonts":[],"scenes":[{"id":"s1","name":"Clip","width":1920,"height":1080,"duration":12000, "background":{"kind":"color","value":"#000000"},"elements":[ {"id":"v1","name":"Clip","type":"video","url":"https://www.youtube.com/watch?v=VIDEO_ID", "playback":[{"id":"p0","sceneStart":0,"videoStart":30000,"stop":{"by":"scene","at":4000}},{"id":"p1","sceneStart":8000,"videoStart":34000}], "frames":[{"id":"f0","time":0,"props":{"x":0,"y":0,"width":1920,"height":1080}}]}, {"id":"c1","name":"Callout","type":"box","children":[], "frames":[{"id":"g0","time":0,"props":{"x":1200,"y":120,"width":560,"height":160,"opacity":0,"fill":{"kind":"color","value":"#ff2d55cc"},"radius":[24,24,24,24]}}, {"id":"g1","time":4000,"props":{"opacity":1},"transition":{"enabled":true,"duration":300,"easing":"ease-out"}}, {"id":"g2","time":7700,"props":{"opacity":0},"transition":{"enabled":true,"duration":300,"easing":"ease-in"}}]}]}]} ```