# Emaki full reference > Browser-local watercolor studio for AI agents. On `/`, `window.emakiStudio` paints `brush`/`wait` JSON on a fixed standard paper: preview, look at the real image, accept, export PNG + scene JSON. Static site: no remote paint API, no built-in model, no cloud sync. This file is enough for a first painting. The human UI is Japanese at `/` and US English at `/en/`; this contract is English. Other backends are not this paper: do not use `window.emaki` (removed local CPU debugger), `emakiSketch`/`astraSketch` (legacy `/sketch/` viewer) or Film/`astraFilm` (WebGL experiment). ## Start 1. Any browser works: the owner's open Emaki tab, a new window, or your own built-in browser. Use `/` (Japanese UI) or `/en/` (US English UI): same paper, storage and API. Prefer a browser the owner can see (not headless or offscreen) so they can watch the painting form. Every `S.preview` draws stroke by stroke on the visible paper with the layer caption. If you can only work headless, send the owner an image after each major stage. Ready when `document.body.dataset.workspaceReady==="true"`. The page's content security policy forbids string eval, so automation helpers that evaluate a string predicate (Playwright's `wait_for_function("...")`) fail; poll with a function evaluate instead. If `window.emakiStudio` never appears, read the text of `#studio-state`. 2. `const S=window.emakiStudio; let o=S.observe({includeImage:false});` Require `o.writable && !o.busy`. Only one Emaki tab per browser profile may write; others are read-only. If `writable` is false, another Emaki tab in this browser holds the paper: work in that tab, or ask the owner to close it and reload. A separate browser (your built-in one, another profile) has its own paper and storage. In the owner's own browser this paper may be their saved artwork: snapshot or export it before `newDocument`. 3. Every mutation takes the CURRENT `expectedRevision`; `accept`/`undo`/`newDocument` return the new `revision`. A stale revision fails. On any error: re-observe, fix the named field, never blindly retry. ## Minimal working example (validated) ```js if (o.marks > 0) await S.execute({op:"snapshot"}); // keep existing art in the trial archive o = await S.newDocument({aspectRatio:"3:4", expectedRevision:o.revision}); const scene = S.scene(); scene.title = "Evening sea"; // detached copy; keep its header scene.layers.push({id:"sky", intent:"Pale sky wash, then let it settle", marks:[ {type:"brush", points:[[0.1,0.2],[0.5,0.18],[0.9,0.2]], width:0.08, color:"#6f9fd8", opacity:0.35, water:0.4, duration:1.2, pressure:[0.6,1,0.5]}, {type:"wait", seconds:5}]}); const t = await S.preview(scene, {expectedRevision:o.revision}); // resolves after rendering; does not adopt // Look at the candidate (see Inspect). Only if it serves your aim: o = await S.accept(t.candidateId, {expectedRevision:t.baseRevision}); await S.exportPNG(); S.exportJSON(); // browser downloads of the ACCEPTED painting await S.exportVideo(); // required: process video of the accepted painting ``` ## API - `S.observe({includeImage:false})` -> `{documentId, revision, view:"current"|"candidate"|"replay"|"rendering"|"video", busy, writable, marks (count of the ACCEPTED scene, not the candidate), canvas, candidateId, render, job, video}`. Plain `S.observe()` adds `displayedImage.dataURL` (a large PNG): use it only to view the image, never as text. - `S.scene()` -> detached copy of the accepted scene. `S.canvasOptions()` -> ratios and fixed paper settings. - `S.preview(scene,{expectedRevision})` -> `{candidateId, baseRevision, jobId, canvas}`. Archives the trial (`candidateId` = its `trialId`). Only the active (latest) candidate can be accepted; completed trials stay in the archive. To adopt an older trial: `t=await S.execute({op:"branch",trialId,expectedRevision:o.revision})` re-renders it as the active candidate, then accept `t.candidateId` with `t.baseRevision`. - `S.exportVideo({secondsPerLayer?:4, subtitles?:false})` records and downloads the process video of the accepted painting (see Finish). - `S.accept(candidateId,{expectedRevision})` adopts that exact computed result. `S.undo({expectedRevision})` goes back ONE step. `S.newDocument({aspectRatio,expectedRevision})` makes a new standard blank (undoable; not a resize). - `S.execute({op,...})` -> `{ok:true,...}` or `{ok:false,error:{message},state}`: trial archive, compare, notes, async jobs. See [workspace.md](/sketch/skills/workspace.md). `ok:false` is never success. - Prefer layer patches for local edits: `append_layer`, `replace_layer`, `remove_layer`, or `preview_patch` with `patch:{title?,appendLayers?,replaceLayers?,removeLayerIds?}`. They preview without resending the fixed scene header or untouched layers. - `S.exportBundle()` downloads accepted PNG + scene JSON and returns their artifact metadata. `S.artifacts()` lists stable re-download URLs for files produced during this page session. - Storage is this browser origin only (localStorage + IndexedDB). Export anything important. ## Scene - Header from `S.scene()` after `newDocument`: `canvas:{profile:"watercolor-standard-v2",aspectRatio}`, `width:720`, height from the ratio, `seed:42`, `material:{model:"paper-brush-v5",resolution:640,roughness:0.26,absorbency:0.42}`. Sheets tagged `watercolor-standard-v1` keep `paper-brush-v3` and paint exactly as before. Never edit or delete it to repair brushwork: changed tagged settings fail validation, and deleting `canvas` turns the scene into an untagged legacy import. 640 is the grid WIDTH. - Ratios: 1:1, 4:5, 3:4, 2:3, 9:16 (default), 5:4, 4:3, 3:2, 16:9. Example 3:4 = 720x960 output, 640x853 grid. Coordinates are fractions of width and height, so never stretch a painting's header to another ratio. - Layer `{id, intent, marks, environment?}`: unique `id` (1-80 chars); `intent` = short truthful caption shown to people during replay and in the video, in the owner's language (keep it to about two short lines); at most 32 layers. - Brush `{type:"brush", points, width, color, opacity, water?, duration?, pressure?}`: `points` = 2-256 centerline `[x,y]` in [0,1] from the top-left (a path, not a fill outline); `width` 0.0001-0.15 of paper width (about width x 720 x pressure px); `color` `"#rrggbb"`; `opacity` 0-1: on v5 the share of paint in the mix, from 0 (clear water) to 1 (almost undiluted tube paint), about one full-strength wash at 0.5; `water` 0-1: on v5 how charged the brush is (default 0.6; 0.15 on v3); `duration` 0.1-20 material seconds (default 0.7): on v5 a faster stroke leaves less water and breaks up sooner; `pressure` 0.05-1, one value per point. - Wait `{type:"wait", seconds}`: 0.1-180. Brush durations + waits <= 7200 material seconds per v5 scene (600 on older papers). On v5 a wash loses its shine in about 3 minutes and is dry in about 18, as on real cold-press paper. Layer `environment:{drying:"hold"}` pauses the drying clock; `"fast"` (v5) is a hair dryer, six times faster. Changing the clock is the one deliberately unreal control. `environment:{tilt:0-60}` (v5) raises the top of the board by that many degrees: wet paint runs down from about 6°. - v5 tools besides the brush: `{type:"spatter", points, spread, drops, size, opacity, color|pigment}` flicks drops; `{type:"salt", points, spread, grains, size}` drops crystals that draw water and loose paint into pale stars; `{type:"mask", points, width, pressure?, duration?}` lays masking fluid, which sets in 600 drying seconds (100 with `"fast"`); `{type:"unmask"}` peels it. Ranges and timing: [material-v5.md](/sketch/skills/material-v5.md). - No fills, pasted reference images, post-filters or image-generation substitutes. `water:0` does not dry paper; only `wait` does. White is not an eraser. On v5 a clean damp brush (opacity 0, low water) lifts wet paint by itself; dried paint must be rewetted and left to soften first, and staining paints leave a stain. Tissue lifting is not implemented. - `tip` (v5) is a physical brush: `round` (sable, points under light pressure), `flat` (chisel edge), `fan` (sparse, streaky), `liner` (long rigger), `hake` (wide goat hair, lots of water), `mop` (squirrel, soft, very absorbent), `hog` (stiff bristle, scrubs and lifts). For your own brush pass `{preset, bristles 4-160, length .3-8, point 0-1, stiffness 0-1, capacity .1-4, spread 0-1.5, thickness .02-1, shape "round"|"flat"|"fan"}`. A drying brush parts into clumps and skips the paper's valleys; that split stroke is the brush, not a pattern. - `pigment` (v5) names a paint: `french-ultramarine`, `phthalo-blue`, `cerulean`, `viridian`, `sap-green`, `hansa-yellow`, `yellow-ochre`, `cadmium-red`, `quinacridone-rose`, `burnt-sienna`, `raw-umber`, `lamp-black`, `chinese-white`, or a mix of up to four `{name, part}` (`part` 0.01-1, relative amounts). Paints differ in opacity (cadmium, ochre, cerulean and white cover; the rest glaze), granulation and staining. Without `pigment`, `color` is a transparent paint of that colour and `granulation` (default 0.15) sets its coarse share. Mixing happens in the paint, not in the colour. - Advanced fields (`granulation`, `tilt`, `angle`, `gesture`, `supply`): read [material-v5.md](/sketch/skills/material-v5.md) and [schema.json](/sketch/skills/schema.json) first. On v5 `supply:{model:"brush-v5",id,action:"reload"|"continue"}` keeps one brush between marks, including colour it drank from the paper. v5 rejects `dry`, `load`, `lift`, `texture`, `softness` and `dynamics`: their effects come from water, pressure, speed and the brush. Older papers still accept them. `constant-v1` supply is removed. - Validation errors name the location, e.g. `layers[0].marks[1]: Invalid drying time: ...`. ## Inspect - Without JavaScript access, use the page's scene form: open `#scene-controls`, edit `#scene-input`, press `#studio-preview`, inspect the sticky live preview, then press `#studio-accept`. Read `#scene-result` for colocated success or error text. Structured commands use `#command-input` and `#command-result`. - After preview the page canvas `#studio-paper` shows the candidate: take a screenshot of it with your browser tool. For a crop: `await S.execute({op:"image", trialId:t.candidateId, crop:[x,y,w,h], maxWidth:720})` returns `{dataURL,width,height}`; crop values are fractions 0-1 of the image, and output is never upscaled beyond the cropped pixels, so enlarge small crops yourself. View or save it as an image. - Judge the whole image plus one relevant crop against your stated aim, right after the stroke and again after waits. Valid JSON, metrics or a finished export are not visual success. - v5 previews resume from the last layer they share with an earlier render in this page session: appending a layer or replacing a late one repaints only from there, while editing an early layer repaints everything after it. Keep finished layers unchanged and work forward. - Long scenes (tool timeouts): `S.execute({op:"submit", scene, expectedRevision})`, poll `{op:"job", jobId}` until `completed`, then accept `job.trialId` with the original revision. ## Method - Work in visible stages: preview each major stage (not one giant final scene) so the owner can follow along, and accept stages that serve the aim. After the final accept you may play the 4x replay (`#studio-play`) for the owner. - Before each major stage, state a short visual aim and what must stay intact. Default order: aim -> small representative swatch -> large masses -> compare with the aim -> needed detail -> final whole-image review -> PNG + JSON. Adapt it deliberately. - Bands, grain and hard edges are failures ONLY when they conflict with the aim; intentional texture is valid. Do not invent an aim afterwards to excuse an accident. - On a mismatch: name the visible gap, pick one plausible cause, read the relevant card, test one change from a saved baseline (keep seed, material, crop and unrelated settings fixed), compare actual images, keep or revert. If trials keep failing, change the hypothesis or technique instead of adding strokes. Stop when the aim is met; disclose tool limits honestly. - Example numbers in cards are trial seeds, not universal settings. A card example is a separate test sheet that replaces the candidate: preview it without accepting, then move its strokes into your own `S.scene()` copy and retest. Reference images are for looking only. - paper-brush-v5 models the physics of paper, water, pigment and brush. Drying times, wet-in-wet spread and the staining (swab) test are checked against published painters' measurements (handprint.com); other constants are estimates. Card numbers were tested on paper-brush-v3. Card `validation` fields are separate claims: `example` = schema checked, `visual` = image evidence, `novice` = fresh-agent evidence. Basic/optional/experimental is a usage category, not a quality score. ## Finish Every finished painting is delivered with ALL of these, in this order: 1. PNG + scene JSON: `await S.exportPNG(); S.exportJSON();` (the accepted painting, even while a candidate or replay is shown). 2. Process video (required, no music yet): `const v=await S.exportVideo();` It starts with 0.5 s of the finished painting, then plays the brush-by-brush process with each layer's `intent` as the caption, and ends on the finished painting. It downloads `emaki-process.mp4` (WebM if the browser cannot record MP4) and returns `{state:"done", file, mimeType, bytes, duration, srt}`. The frames also play on the visible paper while recording. It first repaints the painting frame by frame (on v5 about as long as painting it from blank paper, often minutes), then records in real time (about layers x 4 s + 2.5 s); keep the tab visible. If your tool times out, call `S.execute({op:"video",wait:false})` and poll `S.observe({includeImage:false}).video` until `state` is `done` (or `failed`: read `error`, fix, retry). 3. A short note in the owner's language: aim, main steps, what you rejected and why, known limits. - Exports return file metadata and leave stable re-download links under the paper and in `S.artifacts()` for this page session. Use those links or your browser tool's download output. Never search the owner's personal folders. If your tool still cannot collect files, return `JSON.stringify(S.scene())` and save `S.execute({op:"image", trialId:, maxWidth:720})` as a PNG; say plainly if the video could not be delivered and why. Deliver real files, not data strings. - Music: ALWAYS ask, for every artwork (test paintings included), in the completion reply and in the owner's language, whether to make matching background music with Gemini and add it to the video, e.g. "Would you like me to make matching background music with Gemini and add it to this video? I'll confirm the method and any cost before starting." / 「この絵に合うBGMをGeminiで作って、動画に付けますか? 利用手段と料金は実行前に確認します。」 Never skip the question, even if you cannot generate music yourself. Never make music before the owner explicitly says yes in the conversation; silence is not consent. The silent video is delivered first regardless of the answer. After a yes, follow [video.md](/sketch/skills/video.md): Gemini browser UI under the owner's existing subscription only (check availability, quota and what data leaves the browser); no paid API, credits or upgrade; stop if payment is required. Emaki has no built-in Gemini integration. Never upload or publish. ## Docs - [Skill index](/sketch/skills/agent-index.json): pick 0-2 goals, then fetch only `/sketch/skills/agent/{id}.json`. A card's `sources` link is optional provenance; do not recurse into it. - [Trial workspace](/sketch/skills/workspace.md): snapshot, list, branch, compare, record, image, export, submit/job/cancel, DOM-only controls, legacy imports, WebMCP. - [Studio scene schema](/sketch/skills/schema.json) - [Decisions after a failed trial](/sketch/skills/decisions.md) ## Optional - [Field notes](/sketch/skills/field-notes.md): repeated wash, white-shape or detail mismatches, rejected approaches, lifting limits. - [Wash lab](/sketch/skills/wash-lab.md): unwanted broad-wash bands only; not a style preset. - [Material v5](/sketch/skills/material-v5.md): how the physical paper behaves and how to cause edges, blooms, granulation, dry brush, lifting and glazes. Read before painting on a new sheet. - [Material v4](/sketch/skills/material-v4.md): opt-in experimental paper-brush-v4, kept for comparison with older studies. - [Human skill guide (Japanese)](/sketch/skills/) ## Storage recovery If preview returns `archiveWarning`, the rendered candidate remains available. Check `S.observe({includeImage:false}).archive`: `memory-only` is not durable. Keep the tab open; retry with `S.execute({op:"retry_archive"})`, or export the trial by its ID (main exports are accepted art only). Adoption saves the painting in localStorage before changing revision. See [workspace persistence](workspace.md#persistence-and-editor) for recovery and diagnostics.