Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Morphotype

A type and logo lab. The geometry engine is Rust (`crates/morpho-core`), compiled to WebAssembly and run in a worker. The UI is React/TypeScript (`ui/src`), and materials are WebGL2 shaders (`ui/src/render`). `docs/ARCHITECTURE.md` explains how they fit together.

## Designing in the app: use morpho

Don't read the template or effect sources to learn what exists, and don't screenshot the editor to judge a design. Use the CLI (`docs/AGENT-TOOLS.md`):

```sh
npm run morpho -- serve & # once: keeps a browser warm (drawing then takes < 1 s)
npm run morpho -- catalog # what exists; then: catalog chrome / glitch / star / motion / keyframes / layers
npm run morpho -- describe "UFO poster" # a design as text, changed settings only
npm run morpho -- scene "UFO poster" -o ufo.morpho
npm run morpho -- set ufo.morpho 'Title.gloss=0.4' 'Star.x=-120'
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
```

Prefer `check`, `describe` and text output over images. When you do need to look, ask for one contact sheet (`--frames`, `vary`), not several renders.

## Commands

- `npm run dev`: the app at http://localhost:1420 (builds the engine in debug first)
- `npm test`: Rust and TypeScript unit tests
- `npm run typecheck`
- `npm run build && npm run test:e2e`: browser end-to-end test
- `npm run wasm:dev`: rebuild the engine after Rust changes
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ npm install
| Build the web app | `npm run build` (output in `dist/`) |
| Rust and UI unit tests | `npm test` |
| Browser end-to-end test | `npm run build && npm run test:e2e` |
| Design from the command line, for AI agents and scripts (catalog, describe, edit, render, variations, checks; see [docs/AGENT-TOOLS.md](docs/AGENT-TOOLS.md)) | `npm run morpho -- help` |
| Live render studio (templates → PNG, compare with a reference) | `node scripts/studio.mjs`, then e.g. `curl '127.0.0.1:7799/compare?tpl=UFO%20poster&ref=ref.png&out=diff.png'` |
| Frame cost of moving geometry (1, 10, 50 animated pen lines, on the GPU) | `npx vite --port 1437`, then `node scripts/perf-geo.mjs` |
| Measure an outline on a reference picture | `cargo run --release --example refpath -- mask.pgm` (a white-on-black PGM mask → path data) |
Expand Down Expand Up @@ -155,8 +156,9 @@ src-tauri Desktop shell: system fonts, font install, dialogs
ui/src/engine Worker + client for the engine
ui/src/render WebGL2 renderer, SDF passes and effect shaders
ui/src/components Editor UI
ui/src/agent The morpho CLI's side of the app: catalog, describe, edits, checks
ui/public/fonts Bundled OFL fonts
scripts WASM build and end-to-end test
scripts WASM build, end-to-end test and the morpho CLI
website Landing page (static; Vercel deploys it from main to morphotype.srivtx.xyz)
```

Expand Down
91 changes: 91 additions & 0 deletions docs/AGENT-TOOLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# morpho: the app from the command line

`scripts/morpho.mjs` drives Morphotype without its interface. It's built for AI agents (and scripts) designing in the app: every command answers in a few lines of text or in **one** PNG, so learning what exists, reading a design, editing it and judging it doesn't mean reading source files or taking a pile of screenshots.

```sh
npm run morpho -- help
```

## Commands

| Command | What it gives back | Browser? |
| --- | --- | --- |
| `catalog [query]` | What designs are made of. With no query, a one-screen index. With a section (`materials`, `modifiers`, `shapes`, `motion`, `keyframes`, `layers`, `presets`, `recipes`, `fonts`, `backgrounds`, `canvas`, `templates`), that whole section. With a name (`chrome`, `glitch`, `star`, `"Ice chrome"`), every setting with its range, default and label. Anything else is searched. | no |
| `describe <scene>` | The design as a text tree, top layer first. Each layer is one line: what it is, where it sits, its material, modifiers, motion and keyframes, showing **only settings that differ from the defaults**. | no |
| `scene <template> [-o f]` | Writes a template as an editable `.morpho` file. | no |
| `set <file> <a=b>…` | Edits a `.morpho` file in place and prints each change (`--dry` to only print them). | no |
| `render <scene>` | A PNG. With one time it's that frame (at rest by default). With `--at 0,1,2` or `--frames 8` it's one captioned filmstrip. | yes |
| `vary <scene>` | One PNG with a captioned cell per variation: `--vary` multiplies lists of values, `--variant 'a=1; b=2'` adds hand-made combinations. | yes |
| `check <scene>` | Problems, in words (see below). | yes |
| `compare <scene> <image>` | How close the design is to a reference picture: colour, pixel and layout scores, plus one reference \| design \| difference image. | 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. | — |

`<scene>` 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.

## 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):

```sh
morpho set y2k.morpho 'Topo blob.gloss=0.8' # a material setting
morpho set y2k.morpho '"Serif K".fill="#ff2d55"' # quote names that contain dots
morpho set y2k.morpho 'Pixel 2.pixelate.size=0.1' # the layer's first modifier of that kind
morpho set y2k.morpho 'Spikes.rays=20' # a shape field
morpho set y2k.morpho 'Caption.text="Hello"' # a content field
morpho set y2k.morpho 'Chrome star.motion.loop=spin' # motion is created when missing
morpho set y2k.morpho 'Ball.animate.liquid={"to":1,"period":4,"phase":0,"mode":"swing"}'
morpho set y2k.morpho 'canvas.width=1080' 'timeline.duration=4' 'background.grain=0.2'
```

- **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.
- **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.

## What `check` looks for

**From the project and the engine's build:**
- layers the engine couldn't build;
- layers entirely or partly off the canvas;
- text too small to read;
- unknown materials and invalid colours;
- settings outside their range;
- keyframes that point at nothing;
- entrances or exits timed after the clock ends.

**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.

## Speed

Drawing runs on the GPU by default, with SwiftShader as a fallback if WebGL won't start there. Pass `--cpu` (or set `MORPHO_CPU=1`) for SwiftShader everywhere: the same pixels on every machine, about 4× slower.

While a job runs, the editor's live preview is held still, because its animation loop competes for the main thread.

With `serve` running, timings on an M-series Mac for a 14-layer animated poster:

| Command | Time |
| --- | --- |
| `render` | 0.2 s |
| six-frame filmstrip | 0.3 s |
| six-cell `vary` | 0.8 s |
| full `check` | 1.9 s |

Under `serve`, edits to the app's TypeScript reload the page by themselves. After an engine (Rust) change, run `npm run wasm:dev`.

## How it fits together

| File | What it does |
| --- | --- |
| `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/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. |
| `scripts/morpho.mjs` | The CLI: argument parsing, Vite and Playwright, the `serve` daemon (`MORPHO_PORT`, default 7801). |
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"preview": "vite preview",
"test": "cargo test --workspace --exclude morphotype-app && vitest run",
"test:e2e": "node scripts/e2e.mjs",
"morpho": "node scripts/morpho.mjs",
"typecheck": "tsc --noEmit -p ui",
"tauri": "tauri"
},
Expand Down
17 changes: 17 additions & 0 deletions scripts/e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,23 @@ try {
check(await until((st) => st.mode === 'design' && st.fonts.some((f) => f.name.startsWith('Testa Grotesk'))), 'Use in Design adds the font under its own name');
s = await state();
check(layersOf(s).some((l) => l.content?.font?.startsWith('custom-')), 'a text layer is set in the new font');

// ---- The agent handle the morpho CLI drives: load, edit, draw, vary and check in words ----
const agent = await page.evaluate(async () => {
const a = await window.morphotype.agent();
return a.quietly(async () => {
await a.open('Animated · Y2K set');
const changes = await a.edit(['"Serif K".fill="#ff2d55"', 'Brand.x=2000']);
const still = await a.render({ size: 300 });
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() };
});
});
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]}`);
} catch (e) {
errors.push(`crashed: ${e.message.split('\n').slice(0, 12).join(' / ')}`);
const pages = browser.contexts().flatMap((c) => c.pages());
Expand Down
Loading
Loading