'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".'
Resources
3Install
npx skillscat add blackbelttechnology/pi-agent-dashboard/deck3d Install via the SkillsCat registry.
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 an existing
.pptx: follow `deck3d-from-pptx`
(import, reference renders, then a variant-and-ask step per styling decision). - 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, …).# Titlestarts a slide. Zero headings ⇒ one slide idslide.- The first paragraph after the heading (before bullets) is the subtitle.
- bulletlines are body bullets.- A fenced ```mermaid
flowchart/sequenceDiagramblock becomes the slide's
diagram (harvested in headless chromium). Any other diagram type ⇒ warn + no diagram. <!-- deck3d: {...} -->inline overrides win overdeck.jsonand warn on clobber.
The comment may span several lines or be written as a bullet (- <!-- deck3d: {...} -->);
in every form it is read as an override and is never the subtitle or a bullet.- 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:
deck3d parse talk.md→talk.json(derived IR + emptyoverrides).deck3d validate talk.json→ schema + derived-edit + orphan/prop warnings.deck3d build talk.md -o talk.html→talk.json+talk.html; runscheck
and printsstyle: <n>/<N> slides styled.- Style — mandatory, and the step that decides whether the deck looks
designed.deck3d check talk.html --stylewarnsstyle-defaultsfor every
slide still running only whatparsechose. 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 withdeck3d 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, andprops search --role ambientfor
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: falselimits the reflection to slide content (one less backdrop pass per
frame), andoverrides.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), whileoverrides.deck.floorReflectivity
(0-1, default 1) sets how STRONG the reflection is; all deck-scope only; - grade the frame with a
postcard (deck3d fx list --kind post): add{ "id": "outline", "params": { "parts": "diagram" } }(orselective-bloom,god-rays,depth-of-field,pixelate,ascii, …)
to the slide'seffects[]. One or two per slide; they stack in a fixed
order, andcheckflags declaredconflicts.
- pick a corpus effect (
- Read the
checkfindings. Fix only the suggested key (e.g.overrides.slides["arch"].diagram.scale), then re-run step 3. deck3d snapshot talk.html --slide 5 -o s5.pngto eyeball one slide.- Repeat until
checkis 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);diagram.data replaces as a whole object so labels and values never mix provenance.
Full field list: `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 theadd effect… picker; both rebuild the slide immediately. Underdeck3d 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.jsonIt 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` has params and licences.
parseassigns deterministic defaults (title→swarm, flowchart→tokens,
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: falserestores
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:
{ "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 serveserves media sitting beside the deck (video/demo.mp4), which
is the authoring path.deck3d render deck.json -o deck.html --embed-videoinlines clips asdata:
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 editPaste the printed { id: "local:<name>", sha256 } entry into the slide'seffects. 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 allundefined. 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 withdeck3d fx promote <name> --source <url> --licence <spdx>.
- Conflicts fail
render; mode-incompatible effects are skipped with a warning;
the summedcostwarns over the quality budget (low6 /medium12 /high20).
Deck assets, layer effects and the pptx pipeline
overrides.assets[]—{ id, path, sha256, mime }pins files (PNG/JPEG/GIF/WebP/SVG,
JSON, text,font/ttf|otf) thatrenderembeds once asdata:URIs, so the HTML stays
one offline file.pathmust stay inside the deck directory; a stalesha256failsvalidate/render. Effects read them withctx.assets.get(id)(decoded bitmap, parsed
JSON, text, or a font family name) — also local effects, whose 64 KiB code cap is
unchanged. Font assets use idsfont-<name>. Budget: warn above 25 MB, fail above 200 MB.- Card kind
layerdraws slide content above the backdrop pass.replacesContent
cards (slide-layer) drop the slide's native title/card/bullets/diagram;persistent
cards (frame-chrome) are deck-scope only (overrides.effects), created once outside
every slide group, driven bytransition({from,to,p}). - Built-ins:
slide-layer,frame-chrome,art3d,icon3d,haze(see
`reference/effects.md`);deck3d fx preview <id>renders each
against a packaged stub bundle. - Commands:
deck3d pptx import|art trace|art icon|art cutout|variants render|pick|note|replay|compare
—deck3d --helplists them; the end-to-end procedure isdeck3d-from-pptx. style.jsonbesidedeck.jsonrecords the picks (steps[].variants[],picked,note,round);validatechecks it.servepreviews a candidate at?variant=<step>:<name>(Vcycles) without writing anything.
Props (content illustrations)
deck3d props search <keywords> → candidate table (vendored first, then Poly Pizza).
You pick by relevance then style — code never picks. Then:
- Prefer one pack per deck and
restyle: "palette". deck3d props fetch <source> <id>→ prints theoverrides.props[]entry
(with thesha256) and caches it in.deck3d/props/.- Add the entry to
overrides.props(roles:hero,illustration,ambient,node:<id>), thenbuild/snapshot. deck3d props search <kw> --role ambientfilters to models cheap enough to
instance in the background and prints a readyambiententry.- 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; tunedeck.jsonoverrides. - Never edit derived fields (
slides[]and below) —validatewarnsedited outside overrides; the nextparseloses the change. - If you need a knob that does not exist, propose a schema field — do not hack the HTML.
Pitfalls
parseof a deck with mermaid needs chromium; missing ⇒npx playwright install chromium.render/validatenever need a browser.- Every three.js
typeface.jsonhas 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. Tunelabels.size, not the material. - Placement:
defaults.rail(lineorbittunnelhelixgrid) strings the
slides in space,defaults.spacingsets the gap,layout(splitsplit-reverse) composes one slide,cardOffsetnudges the card. Every rail
frames a slide identically, so a rail switch never needs a per-slide re-tune. - Transitions = how the camera ENTERS a slide. Set
defaults.transition, per
slideoverrides.slides["<key>"].transition, or deck-leveldefaults.transitionCycle: ["dolly","crane",...](slides without their own
transition takecycle[index % length]).durationSecandtransitionAmount(0–2, scales offsets/roll/lens; 0 ≈ dolly) per deck/slide.
Motions live inruntime/transition.ts(makeMotion), validated as an enum:id move dolly(default)straight eased move to the anchor swingdolly + sideways arc away from the mid-point revolveswings wide to travel-right, locks on early, arcs in around the slide cranerises over the gap (~0.3 × travel length), descends onto the slide bankdolly with roll into the move and back out (≤ 8°) flythroughlow pass through the gap, slight bank, wider lens fadescreen covers with the palette bg, camera switches, uncovers iriscircular mask closes to centre, camera switches, reopens cutinstant snap; old slide's local fx disposed at once pathauthor keyframes from the slide's cameraPathcameraPath(needstransition: "path", else validate fails):
Keys (1–8,"ertek": { "transition": "path", "durationSec": 2.2, "cameraPath": { "ease": "inOut", "keys": [ { "t": 0.3, "offset": [0.18, 0.22, 0], "look": 0.25, "roll": 6 }, { "t": 0.68, "offset": [-0.08, 0.1, 0.04], "look": 0.9, "fov": 48 } ] } }tstrictly increasing in (0,1)) sit in the TRAVEL FRAME:offset = [right, up, forward]as fractions of the from→to camera distance,
so a path reads the same on every rail/spacing; start and end are implied and
pinned (always lands exactly).look0 = leaving slide, 1 = arriving;roll
±45°;fov20–90°. Back navigation plays the LEAVING slide's move reversed
(mirror image). Both slides stay un-culled for the whole move. Camera-pinned
local fx (overlays parented viaonBeforeRender) tilt withroll— keep
roll small on decks with HUD-like overlays.railstays deck-wide.
Every flying move cross-fades: the leaving slide's backdrop (local fx +
background) fades out over 5–60 % of the move, the arriving one fades in over
10–70 % (whole group when it was culled). Local fx that set their own
opacity still compose (fader re-bases); custom shaders without anopacityuniform only pop at the ends — exposeuniforms.opacity. - During a fly both slides tick, the arriving slide's local fx are re-created
at the START of the fly, andcullNeighbours()hides whole slide groups by
distance — a camera-pinned overlay that must survive the fly has to reparent
itself into the scene (keep shared state inTHREE.Cache.files) — or, for deck-wide
camera-pinned art, use a persistent card (frame-chrome), which the runtime creates
once outside every slide group and drives withtransition({from,to,p}). - 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 tosetTimeoutso snapshots capture a finished frame. - A
check: cleandeck can still be bare.checkmeasures fit and
legibility, not ambition — read thestyle: <n>/<N> slides styledline and
runcheck --stylebefore calling a deck done.
Contributing an effect
Development-time procedure on the package (distinct from the deck tune loop):
- 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. - Port to the module interface:
create(ctx, params) → { object?, pass?, material?, tick?, dispose() }. - Write the card
src/fx/<id>.meta.json(kind, tags, cost, modes, params schema,
conflicts,sourceURL,licence). deck3d fx preview <id>→ check the PNG is not black.npm run gen:effects→ regeneratereference/effects.md.npm test→ the corpus test validates every card and the catalogue hash.