v0.3.0-alpha: Chromium rasterizer (R4), branching, chapters

- Rasterizer: playwright-core + system Chromium renders dom steps to PNG
  (1280x800, JavaScript disabled — snapshots stay inert). Cached permanently
  on the step (dom_html immutable). CHROMIUM_PATH configurable.
- Video render now includes DOM steps as real frames (verified h264 1280x800,
  0 skipped); guides now show rasterized screenshots for DOM steps
- Branching: per-step branch hotspots (dashed, labeled) jumping to any target
  step; lead gates respected on branch jumps
- Chapters: demo-level chapter menu in player bar
- API: PATCH /api/demos/:id (name, chapters), branches on step patch, both in
  public JSON
- MCP surface: 15 tools (+set_step_branches, +set_chapters)
- Docker: chromium + fonts in runtime image, CHROMIUM_PATH preset
This commit is contained in:
Scot Thom 2026-07-18 05:36:27 +00:00
parent a535e15253
commit 928628f49a
11 changed files with 269 additions and 27 deletions

View file

@ -41,6 +41,17 @@
}
snap.appendChild(container);
(step.branches || []).forEach(function (br) {
var bh = document.createElement("div");
bh.className = "hotspot";
bh.style.left = br.x + "%"; bh.style.top = br.y + "%";
bh.style.width = br.w + "%"; bh.style.height = br.h + "%";
bh.style.borderStyle = "dashed";
if (br.label) bh.title = br.label;
bh.addEventListener("click", function () { jumpTo(br.target_step_id); });
snap.appendChild(bh);
});
if (step.hotspot) {
var h = document.createElement("div");
h.className = "hotspot";
@ -112,6 +123,19 @@
snap.appendChild(overlay);
}
function jumpTo(stepId) {
var idx = state.demo.steps.findIndex(function (s) { return s.id === stepId; });
if (idx < 0) return;
var cur = state.demo.steps[state.i];
if (cur.lead_gate && cur.lead_gate.enabled && !gatePassed[cur.id]) {
showGate(cur, function () { jumpTo(stepId); });
return;
}
state.i = idx;
beacon("advance", stepId);
render();
}
function next() {
var cur = state.demo.steps[state.i];
if (cur.lead_gate && cur.lead_gate.enabled && !gatePassed[cur.id]) {
@ -145,6 +169,18 @@
.then(function (demo) {
state.demo = demo;
titleEl.textContent = demo.name;
if (demo.chapters && demo.chapters.length) {
var sel = document.createElement("select");
sel.style.cssText = "border:1px solid #e2e8f0;border-radius:6px;padding:5px 8px;font:inherit;max-width:180px";
var opt0 = document.createElement("option");
opt0.textContent = "Chapters"; opt0.value = ""; sel.appendChild(opt0);
demo.chapters.forEach(function (c) {
var o = document.createElement("option");
o.value = c.step_id; o.textContent = c.title; sel.appendChild(o);
});
sel.addEventListener("change", function () { if (sel.value) { jumpTo(sel.value); sel.value = ""; } });
titleEl.parentNode.insertBefore(sel, titleEl.nextSibling);
}
document.title = demo.name;
beacon("view", demo.steps.length ? demo.steps[0].id : null);
render();