deck3d · diff
git:20260918.d661546 to git:20260929.4aa0939
116 added, 6 removed. Audit A to A.
---
name: deck3d
description: 'Build a self-contained 3D presentation from a Markdown outline — parse, tune via `overrides`, render to one offline deck.html. Use on "make a 3D deck from this outline", "build a presentation from these notes", "deck3d this markdown", "turn this talk into a 3D deck".'
---
# deck3d
Turn a Markdown outline into one self-contained, offline `deck.html`: extruded 3D
titles, harvested mermaid diagrams built as 3D objects, animated backgrounds, and
an optional glTF prop. The converter code is deterministic; **you tune the result
by writing `overrides` in `deck.json`**, never by editing the HTML.
## When to use
- The user has a Markdown outline (headings + bullets + ```mermaid blocks) and
wants a presentation they can open offline in a browser.
- You need to tune placement, legibility, mode, effects or props of an existing
`deck.json`.
## Markdown grammar
- `---` front-matter: deck defaults (`mode`, `palette`, `material`, `quality`, `transition`, …).
- `# Title` starts a slide. Zero headings ⇒ one slide id `slide`.
- The first paragraph after the heading (before bullets) is the subtitle.
- `- bullet` lines are body bullets.
- A fenced ```mermaid `flowchart` / `sequenceDiagram` block becomes the slide's
diagram (harvested in headless chromium). Any other diagram type ⇒ warn + no diagram.
- `<!-- deck3d: {...} -->` inline overrides win over `deck.json` and warn on clobber.
- Pin a slide id with `# Final {#outro}` so renames never orphan tuning.
## The tune loop
+ Interactive authoring path: `deck3d serve talk.md [--check]`. Watches sources,
+ rebuilds on change, reloads in place, re-pins local fx hashes, and lets the
+ configurator save or apply overrides directly.
+
+ Manual headless / agent / CI path:
+
1. `deck3d parse talk.md` → `talk.json` (derived IR + empty `overrides`).
2. `deck3d validate talk.json` → schema + derived-edit + orphan/prop warnings.
- 3. `deck3d build talk.md -o talk.html` → `talk.json` + `talk.html`; runs `check`.
- 4. Read the `check` findings. **Fix only the suggested key** (e.g.
+ 3. `deck3d build talk.md -o talk.html` → `talk.json` + `talk.html`; runs `check`
+ and prints `style: <n>/<N> slides styled`.
+ 4. **Style** — mandatory, and the step that decides whether the deck looks
+ designed. `deck3d check talk.html --style` warns `style-defaults` for every
+ slide still running only what `parse` chose. For each one:
+ - pick a corpus effect (`deck3d fx list --topic geo`, `--kind background`;
+ the stylistic set — `clipped-solids`, `extruded-shapes`, `volume-cloud`,
+ `curve-flow`, … — is never auto-picked, so it is always an explicit choice)
+ *or* write a per-deck one with `deck3d fx scaffold <name> --for <slideId>`;
+ - give a content slide without a mermaid block a built topology
+ (`overrides.slides["<id>"].diagram.kind` + `data`);
+ - illustrate the section: `deck3d props search <kw> --role hero`,
+ `--role illustration`, and `props search --role ambient` for
+ low-triangle background instances;
+ - if extruded titles read as a solid block, set `titleEdge: "contrast"`
+ (deck default, or per slide) — it outlines every character in the
+ palette text colour;
+ - for a deck that wants a horizon, set `overrides.deck.floor: "water"`
+ (one global surface; per-slide is not a thing);
+ - the floor reflects the backgrounds too; `overrides.deck.reflectBackdrop:
+ false` limits the reflection to slide content (one less backdrop pass per
+ frame), and `overrides.deck.floorMatte` (0-1) roughens it — 0 is a perfect
+ mirror, ~0.35 reads as polished concrete, ~0.85 as scattered stone (the
+ `⚙` panel drags it on a slider), while `overrides.deck.floorReflectivity`
+ (0-1, default 1) sets how STRONG the reflection is; all deck-scope only;
+ - grade the frame with a `post` card (`deck3d fx list --kind post`): add
+ `{ "id": "outline", "params": { "parts": "diagram" } }` (or
+ `selective-bloom`, `god-rays`, `depth-of-field`, `pixelate`, `ascii`, …)
+ to the slide's `effects[]`. One or two per slide; they stack in a fixed
+ order, and `check` flags declared `conflicts`.
+ 5. Read the `check` findings. **Fix only the suggested key** (e.g.
`overrides.slides["arch"].diagram.scale`), then re-run step 3.
- 5. `deck3d snapshot talk.html --slide 5 -o s5.png` to eyeball one slide.
- 6. Repeat until `check` is clean (or only acceptable contrast warnings remain).
+ 6. `deck3d snapshot talk.html --slide 5 -o s5.png` to eyeball one slide.
+ 7. Repeat until `check` is clean **and** the style line accounts for every slide
+ you meant to design.
Tune by writing `deck.json`'s `overrides` only:
```
overrides: {
deck: { ...defaults knobs... },
effects: [{ id, params? }],
props: [{ source, id, licence, author, sha256, slide, role, size?, count?, restyle?, anim? }],
slides: { "<slideId>": { mode?, palette?, material?, scene?, quality?,
diagram: { scale?, offset? }, camera: { distance? }, labels: { size? },
check: { ignore? }, effects? } },
nodes: { "<slideId>/<nodeId>": { shape?, label?, position?, size?, material? } },
edges: { "<slideId>/<edgeId>": { kind?, material? } }
}
```
- Objects deep-merge; **arrays replace** (an `effects`/`props` list is the whole list).
+ Objects deep-merge; **arrays replace** (an `effects`/`props` list is the whole list);
+ `diagram.data` replaces as a whole object so labels and values never mix provenance.
Full field list: [`reference/ir-fields.md`](reference/ir-fields.md).
+ The deck's `⚙` configurator (`C` key, or the gear) tries knobs live. Its **Effects**
+ block ticks composed effects off and adds any corpus or `local:` card from the
+ `add effect…` picker; both rebuild the slide immediately. Under
+ `deck3d serve`, **Apply to deck.json** merges straight into `talk.json` and rebuilds;
+ **Save overrides.json** writes `overrides.json` beside the deck. In unserved decks
+ or headless workflows, **Export** writes an `overrides.json` in exactly this grammar.
+ Merge it manually with:
+
+ ```
+ deck3d overrides apply talk.json overrides.json
+ ```
+
+ It deep-merges, re-validates, and refuses to write on a validation error. Note
+ that **markdown inline overrides win** over anything in that file, and an
+ exported `effects` list pins that scope's whole list.
+
## Styling with effects
`deck3d fx list [--kind k] [--tag t] [--json]` prints the catalogue;
[`reference/effects.md`](reference/effects.md) has params and licences.
- `parse` assigns deterministic defaults (title→`swarm`, flowchart→`tokens`,
- sequence→`rings`, security→`glyph-rain`, data→`data-columns`).
+ sequence→`rings`, security→`glyph-rain`, data→`data-columns`), then routes the
+ rest by topic (`deck3d fx list --topic <t>`; `ai agents geo trust security
+ compute data money work timeline sales process`). `autoStyle: false` restores
+ the plain v1 fallback.
- Replace them with `overrides.slides["<id>"].effects = [{ id, params? }]`.
+
+ ### Video on a slide
+
+ `video-screen` puts a looping clip on a framed screen standing in the scene:
+
+ ```json
+ { "id": "video-screen", "params": { "src": "video/demo.mp4", "width": 7.5, "x": 5, "y": 1, "tilt": -0.25 } }
+ ```
+
+ The clip MUST be same-origin, or the browser taints the canvas and the frame
+ never renders. Two ways to satisfy that:
+
+ - `deck3d serve` serves media sitting beside the deck (`video/demo.mp4`), which
+ is the authoring path.
+ - `deck3d render deck.json -o deck.html --embed-video` inlines clips as `data:`
+ URLs for ONE portable file. Opt-in, because base64 costs ~33% on top of the
+ clip's own size.
+
+ A bare `file://deck.html` next to a `video/` folder does NOT work — that clip is
+ cross-origin. Playback freezes during transitions and runs only while the slide
+ is settled, so `check` and `snapshot` stay deterministic.
+
+ ### Per-deck (local) effects
+
+ When no corpus effect fits the topic, write one beside `deck.md`:
+
+ ```
+ deck3d fx scaffold neural-mesh --for ai # writes fx/neural-mesh.{js,meta.json}
+ deck3d fx preview local:neural-mesh --palette ember -o p.png
+ deck3d fx hash neural-mesh # re-pin after every edit
+ ```
+
+ Paste the printed `{ id: "local:<name>", sha256 }` entry into the slide's
+ `effects`. `render` embeds the source and refuses a stale hash, so the deck
+ stays byte-deterministic and offline.
+
+ Inside a local module the deck shadows the non-deterministic and I/O globals:
+ `Math.random` **is the deck's seeded per-slide generator**, and `window`,
+ `document`, `fetch`, `setTimeout`, `Date`, `Promise`, `eval` and friends are all
+ `undefined`. No `import`, one default export, 64 KiB cap. A throw disables that
+ effect only and shows up as `local-fx-error` in `check`; any network the deck
+ attempts shows up as `local-fx-network`. Promote a good one with
+ `deck3d fx promote <name> --source <url> --licence <spdx>`.
- Conflicts fail `render`; mode-incompatible effects are skipped with a warning;
the summed `cost` warns over the quality budget (`low` 6 / `medium` 12 / `high` 20).
## Props (content illustrations)
`deck3d props search <keywords>` → candidate table (vendored first, then Poly Pizza).
**You** pick by relevance then style — code never picks. Then:
1. Prefer one pack per deck and `restyle: "palette"`.
2. `deck3d props fetch <source> <id>` → prints the `overrides.props[]` entry
(with the `sha256`) and caches it in `.deck3d/props/`.
3. Add the entry to `overrides.props` (roles: `hero`, `illustration`,
`ambient`, `node:<id>`), then `build`/`snapshot`.
+ 4. `deck3d props search <kw> --role ambient` filters to models cheap enough to
+ instance in the background and prints a ready `ambient` entry.
+ 5. No model fits? `deck3d props generate --prompt "<text>" --name <n>` (or
+ `--from-image`). Optional python path; network at authoring time only.
Licences: CC0 needs no credits; CC-BY (and any non-CC0, non-`generated`) produces a
last `credits` slide at render. Never fetch a model whose licence you cannot name.
## Forbidden edits
- **Never edit `deck.html`** — it is regenerated; tune `deck.json` `overrides`.
- **Never edit derived fields** (`slides[]` and below) — `validate` warns
`edited outside overrides`; the next `parse` loses the change.
- If you need a knob that does not exist, propose a schema field — do not hack the HTML.
## Pitfalls
- `parse` of a deck **with mermaid needs chromium**; missing ⇒
`npx playwright install chromium`. `render`/`validate` never need a browser.
- Every three.js `typeface.json` has corrupt `ő ű Ő Ű` glyphs; deck3d uses a real
Poppins TTF through opentype.js — do not swap in a typeface.json.
- Extruded small text blooms and is unreadable; diagram labels are flat canvas
planes with a background-colour outline. Tune `labels.size`, not the material.
+ - Placement: `defaults.rail` (`line` `orbit` `tunnel` `helix` `grid`) strings the
+ slides in space, `defaults.spacing` sets the gap, `layout` (`split`
+ `split-reverse`) composes one slide, `cardOffset` nudges the card. Every rail
+ frames a slide identically, so a rail switch never needs a per-slide re-tune.
- The mermaid engine is pinned exactly; a bump is a deliberate change and the
harvest fixtures will fail if the rendered id scheme moves.
- Headless/background tabs stall `requestAnimationFrame`; the loop falls back to
`setTimeout` so snapshots capture a finished frame.
+ - **A `check: clean` deck can still be bare.** `check` measures fit and
+ legibility, not ambition — read the `style: <n>/<N> slides styled` line and
+ run `check --style` before calling a deck done.
## Contributing an effect
Development-time procedure on the package (distinct from the deck tune loop):
1. Search a source; check its licence against the allow-list: **MIT, Zlib,
BSD-2/3-Clause, CC0-1.0, Apache-2.0, OFL-1.1**. LYGIA (Prosperity) and
Shadertoy content (default CC BY-NC-SA) are **inspiration only** — never port
them verbatim.
2. Port to the module interface: `create(ctx, params) → { object?, pass?, material?, tick?, dispose() }`.
3. Write the card `src/fx/<id>.meta.json` (kind, tags, cost, modes, params schema,
conflicts, `source` URL, `licence`).
4. `deck3d fx preview <id>` → check the PNG is not black.
5. `npm run gen:effects` → regenerate `reference/effects.md`.
6. `npm test` → the corpus test validates every card and the catalogue hash.