diff --git a/CLAUDE.md b/CLAUDE.md index b79883e..4872f13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,13 @@ npm run morpho -- render ufo.morpho --frames 6 # one filmstrip image npm run morpho -- vary ufo.morpho --vary 'Title.liquid=0.2,0.6,1.2' npm run morpho -- check ufo.morpho # problems in words npm run morpho -- compare ufo.morpho reference.png + +npm run morpho -- new poster.morpho --canvas 1080x1350 --bg "#111111" # from scratch +npm run morpho -- add poster.morpho text "HELLO" --size 200 --preset "Ice chrome" +npm run morpho -- align poster.morpho HELLO # centre by what's drawn +npm run morpho -- layout poster.morpho # boxes and margins as text +npm run morpho -- timing poster.morpho # when everything moves +npm run morpho -- video poster.morpho -o poster.mp4 ``` Prefer `check`, `describe` and text output over images. When you do need to look, ask for one contact sheet (`--frames`, `vary`), not several renders. diff --git a/docs/AGENT-TOOLS.md b/docs/AGENT-TOOLS.md index c29bfba..af7cbf5 100644 --- a/docs/AGENT-TOOLS.md +++ b/docs/AGENT-TOOLS.md @@ -18,10 +18,38 @@ npm run morpho -- help | `vary ` | One PNG with a captioned cell per variation: `--vary` multiplies lists of values, `--variant 'a=1; b=2'` adds hand-made combinations. | yes | | `check ` | Problems, in words (see below). | yes | | `compare ` | How close the design is to a reference picture: colour, pixel and layout scores, plus one reference \| design \| difference image. | yes | +| `new ` | A blank design: `--canvas 1080x1350` or a preset name (`"Instagram post"`), `--bg "#111111"`, `"#a,#b"` (a gradient) or a background preset, `--duration 4`. | no | +| `add text "HI"` · `shape star` · `line ""` · `path ""` | A new layer. Options: `--name`, `--at x,y`, `--size`, `--font`, `--color`, `--material chrome` or `--preset "Ice chrome"`, `--recipe Glitch`, `--rotation`, `--scale`, `--width` (lines), and `--above L`, `--below L` or `--into Group` (the default is on top). Lines and outlines take SVG path data in em units, centred on 0,0. | no | +| `add modifier glitch --to L` | A modifier with its defaults; `--fields '{"amount":0.6}'` overrides them. A recipe name adds that recipe. | no | +| `add group A B` | Wraps layers in a group where the topmost one was (`--merge` fuses them into one shape). | no | +| `add font f.ttf` · `image p.png` | Embeds a font and prints its id for `--font`. Places a picture in a rectangle, or with `--to L` fills that layer with it. | yes | +| `remove` · `duplicate` · `arrange` | `remove L…`. `duplicate L` (new ids, so its keyframes follow its own modifiers). `arrange L top\|bottom\|up\|down\|above M\|below M`. | no | +| `align L… [--to canvas\|M] [--edge centre,middle]` | Lines layers up by what they actually draw, not their anchors. A wavy or italic word that looks off centre gets centred by its ink. | yes | +| `layout ` | Every layer's box, centre and margins to each edge as text, plus the layout notes `check` gives. | yes | +| `timing ` | When everything moves: entrances and exits as bars on the clock, loops and keyframes as their cycles (hold keyframes as rest, go, at To, back), with notes on rhythm (entrances all starting at once, long stretches where nothing moves, moves cut off by the clock). | no | +| `video -o f.mp4` | MP4, MOV, WebM or GIF (needs ffmpeg), or PNG frames into a folder ending in `/`. Every frame is drawn at its exact time, so a 4 s loop exports in about 3 s and joins seamlessly. `--fps` (default 30, GIF 20), `--size` (GIF default 480) or `--scale`. | yes | +| `fit L.setting…` | Tunes each setting in turn until the design matches a reference picture, writes the file (or `--out`), and prints before → after. Settings: position, rotation, scale, and any number in the content or material. | yes | | `serve` / `stop` | Keeps Vite and a browser running. While it's up, drawing commands take well under a second instead of 5–15 s. | — | `` is a template name (exact, any case, or a unique part of one) or a `.morpho`/`.json` file. PNGs go to `test-results/morpho/` unless you pass `--out`. `--json` gives machine-readable output. +## A design from scratch + +```sh +morpho new gig.morpho --canvas 1080x1350 --bg "#0d0d12" --name "Night Drive" --duration 4 +morpho add gig.morpho text "NIGHT\nDRIVE" --font unbounded --size 300 --preset "Ice chrome" --at 0,-150 --name Title +morpho add gig.morpho shape ring --size 900 --at 0,-150 --color "#5a17d6" --below Title --name Halo +morpho add gig.morpho shape sparkle --size 220 --at 320,-490 --preset Plasma --name Spark +morpho add gig.morpho text "LIVE · 21.11 · WAREHOUSE 9" --font space-grotesk --size 34 --color "#e8e8f0" --at 0,420 --name Info +morpho add gig.morpho modifier wave --to Title --fields '{"amplitude":0.04,"wavelength":1.4}' +morpho set gig.morpho 'Spark.motion.loop="spin"' 'Spark.motion.period=4' 'Halo.motion.loop="pulse"' 'Title.motion.enter="rise"' +morpho check gig.morpho # → title 10% off both edges; the loop jumps (the entrance replays) +morpho set gig.morpho 'Title.size=250' 'Title.motion.leave="fade"' 'Title.motion.leaveAt=3.3' +morpho align gig.morpho Title Info # centre by what's drawn +morpho check gig.morpho # → no problems; the loop joins smoothly +morpho video gig.morpho -o gig.mp4 +``` + ## Editing: `Layer.setting=value` Use the same assignments for `set`, and for `--set`, `--vary` and `--variant` on any command (with `--set`, files are left alone): @@ -40,6 +68,8 @@ morpho set y2k.morpho 'canvas.width=1080' 'timeline.duration=4' 'background.grai - **Addressing a layer.** Use its name or its `#id`. Bare keys find their own home: a material setting, a content field, a shape field, or a modifier kind. A full path (`style.params.gloss`, `modifiers.0.amount`) also works. - **Values.** A value is JSON when it parses as JSON, and plain text otherwise. `null` removes a key. - **Checks.** Numbers are checked against the setting's range. Unknown layers or settings fail with a list of the names that would have worked. +- **Presets and recipes.** `Layer.preset="Ice chrome"` switches to that look. `Layer.recipe=Glitch` adds that recipe's modifiers. +- **Motion kinds.** Setting `motion.loop`, `motion.enter` or `motion.leave` also sets that kind's own default amount (spin counts turns, sway counts degrees). Set the amount after the kind. - **Changing the material.** Setting `effect` switches the material. Settings the old and new materials share are kept; the rest start at the new material's defaults. - **Template ids.** They are generated afresh each time a template is made, so address template layers by name. After `scene`, the ids in the file stay fixed. @@ -54,12 +84,21 @@ morpho set y2k.morpho 'canvas.width=1080' 'timeline.duration=4' 'background.grai - keyframes that point at nothing; - entrances or exits timed after the clock ends. +**From the boxes (layout notes):** +- edges and centres that nearly line up, 0.2–1.2% of the canvas apart (closer than that, nobody sees it); +- layers that are nearly centred on the canvas; +- things crowding an edge; +- a design that sits a little off centre; +- two headlines of about the same size (no clear lead); +- too many text sizes. + **From the pixels:** - layers that can't be seen at rest: each layer is hidden in turn and the image compared, scaled to the layer's size so hairlines and tiny marks count; - text with low contrast against what's actually behind it; - a blank, one-colour design; -- sudden jumps in the motion (frame pairs one video frame apart, sampled across the clock); -- a loop that jumps where it starts over. +- pops in the motion: one frame step far bigger than the steps either side of it, so fast but smooth moves aren't flagged; +- a loop that jumps where it starts over (for example an entrance that replays every loop); +- where the visual weight sits, and how many hues the palette really uses. ## Speed @@ -85,6 +124,9 @@ Under `serve`, edits to the app's TypeScript reload the page by themselves. Afte | `ui/src/agent/catalog.ts` | Generates the catalog from the same registries the Inspector draws from (`EFFECTS`, `PRESETS`, `MODIFIER_UI`, `SYMBOL_UI`, `LOOP_UI`, …), so new materials, modifiers and shapes show up without touching it. A test fails if the layer-format notes miss a field that the layer factories create. | | `ui/src/agent/describe.ts` | The text tree. | | `ui/src/agent/edit.ts` | Assignments and their checks. | +| `ui/src/agent/build.ts` | New designs, layers, modifiers, groups, duplicates and restacking. | +| `ui/src/agent/layout.ts` | Boxes, layout notes, balance and palette. | +| `ui/src/agent/timing.ts` | The timing chart and rhythm notes (from the timeline dock's own tracks). | | `ui/src/agent/check.ts` | Structural checks and the pixel measures (pure, unit-tested). | | `ui/src/agent/page.ts` | The in-app side, on `await window.morphotype.agent()` (its own chunk). Loads a project with its fonts and pictures, builds it at export quality, and draws frames through the renderer's export path. Contact sheets are composed in an `OffscreenCanvas`. | | `ui/src/agent/node.ts` | What the no-browser commands load in Node through Vite's SSR loader. | diff --git a/scripts/e2e.mjs b/scripts/e2e.mjs index 7bd6a36..52d8960 100644 --- a/scripts/e2e.mjs +++ b/scripts/e2e.mjs @@ -904,12 +904,18 @@ try { const strip = await a.render({ at: [0, 1, 2], size: 200 }); const grid = await a.vary([{ label: 'blue', edits: ['"Serif K".fill="#2347ff"'] }, { label: 'red', edits: [] }], { size: 200 }); const report = await a.check({ size: 240, layers: 4 }); - return { changes, still: [still.width, still.height], strip: [strip.width, strip.height], grid: [grid.width, grid.height], report, text: a.describe() }; + const layout = await a.layout(); + const video = await a.frameBatch({ fps: 10, from: 0, count: 3, size: 120 }); + const aligned = await a.align(['Caption'], 'canvas', ['centre']); + return { changes, still: [still.width, still.height], strip: [strip.width, strip.height], grid: [grid.width, grid.height], report, text: a.describe(), layout, video: [video.frames.length, video.total], aligned: aligned.change }; }); }); check(agent.changes.length === 2 && agent.text.includes('flat {fill #ff2d55}'), `agent edits the design (${agent.changes[0]})`); check(agent.still[0] === 300 && agent.strip[0] > 600 && agent.grid[0] > 400, `agent draws a still ${agent.still.join('×')}, a filmstrip ${agent.strip.join('×')} and a grid ${agent.grid.join('×')}`); check(/✗ "Brand" .*entirely outside the canvas/.test(agent.report), `agent check finds the layer moved off the canvas: ${agent.report.split('\n')[0]}`); + check(/margins L .* R /.test(agent.layout) && /palette: /.test(agent.report), 'agent layout lists boxes and margins, and check reads the palette'); + check(agent.video[0] === 3 && agent.video[1] === 40, `agent draws video frames in batches (${agent.video.join(' of ')})`); + check(/Caption" x:|already aligned/.test(agent.aligned), `agent aligns by what's drawn (${agent.aligned})`); } catch (e) { errors.push(`crashed: ${e.message.split('\n').slice(0, 12).join(' / ')}`); const pages = browser.contexts().flatMap((c) => c.pages()); diff --git a/scripts/morpho.mjs b/scripts/morpho.mjs index 8423b0c..5e9db86 100644 --- a/scripts/morpho.mjs +++ b/scripts/morpho.mjs @@ -7,6 +7,11 @@ // npm run morpho -- describe the design as a text tree (changed settings only) // npm run morpho -- scene