# Interactive slides

> Slides drawn or driven by code in a MakoDeck deck: the `window.Mako` API, the site's JavaScript libraries, mirroring widgets to the audience, and the rules thumbnails and the PDF depend on.

Add code only when it helps the story — a chart that builds, a model to turn, a live calculator. A static slide is easier to read, print and trust. Everything on the sandbox page (https://makodeck.com/ai/sandbox.md) applies: data is frozen at upload, and nothing loads from other sites.

## Where code runs

Each window runs its own copy of the deck: the deck's page, the presenter window, the audience window, and the site's own renders for thumbnails and the PDF. `window.Mako` keeps them in agreement. It is defined before any of your scripts run; don't declare your own `Mako`.

| Member | What it is |
|---|---|
| `Mako.seed` | A whole number, the same in every window, after a replacement and in copies. Pin it with `"seed"` in the manifest. |
| `Mako.still` | `true` when the deck must show its finished frame at once: thumbnails, the PDF, and viewers who prefer reduced motion. |
| `Mako.role` | `"render"` (thumbnails and PDF), `"viewer"`, `"presenter"` or `"audience"`; `null` until the site says. |
| `Mako.random()` | A seeded random number in [0, 1). |
| `Mako.onSlide(target, enter)` | Runs `enter(ctx)` each time the slide holding `target` (an element, or a 0-based slide index) becomes active. Returns an unsubscribe function. |
| `Mako.share(state)` / `Mako.onShared(fn)` | Mirror a widget's state from the presenter's window to the audience's. |
| `Mako.requestDownload(path)` | Ask the site to offer one of the deck's attached files for download. |

`enter(ctx)` receives `index` and `slide`; `still`; `signal`, an `AbortSignal` that fires when the slide is left; `random()`, a fresh seeded stream for this slide and handler (the same picture in every window, whatever order slides are visited in); and `share` and `onShared` scoped to this slide. Return a Promise when drawing takes time: the site waits for it, at most 6 seconds per slide for thumbnails and 20 seconds in all for the PDF.

## How the site runs code-drawn slides

- **Thumbnails:** the site enters each slide in turn with `still` true; leaving a slide aborts its `signal`.
- **The PDF:** the site then calls `enter` again for every slide that has an `onSlide` handler, with `still` true, and never aborts those runs. So when `still` is true, draw the final state at once; cleaning up on `signal` (such as `chart.destroy()`) is fine.
- Where you can, give a code-drawn slide a fallback that shows without code — the numbers as text, or a static SVG — in case drawing fails.

## A chart that draws in

```html
<section class="slide slide--content" id="growth" aria-roledescription="slide" aria-labelledby="growth-title">
  <div class="header"><p class="kicker">Growth</p><h2 id="growth-title">Orders doubled in a year</h2></div>
  <div class="body"><canvas id="growth-chart" width="1600" height="680" role="img" aria-label="Orders per quarter: 120, 150, 190, 240"></canvas></div>
</section>
…
<script src="_mako/lib/chart.js@4.5.1/chart.umd.min.js"></script>
<script>
  const canvas = document.getElementById("growth-chart");
  const token = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
  Mako.onSlide(canvas, async ({ still, signal }) => {
    await document.fonts.load('24px "Inter"'); // the brand font, as named in the CSS
    Chart.getChart(canvas)?.destroy(); // one chart per canvas
    Chart.defaults.font.family = token("--brand-font");
    Chart.defaults.font.size = 24;
    const chart = new Chart(canvas, {
      type: "bar",
      data: { labels: ["Q1", "Q2", "Q3", "Q4"], datasets: [{ data: [120, 150, 190, 240], backgroundColor: token("--brand-accent") }] },
      options: { responsive: false, animation: still ? false : { duration: 1500 }, layout: { padding: { top: 40 } }, plugins: { legend: { display: false } } },
      plugins: [{ id: "values", afterDatasetsDraw(c) { // a value on each bar
        const g = c.ctx; g.save(); g.textAlign = "center"; g.fillStyle = token("--brand-ink"); g.font = `600 24px ${token("--brand-font")}`;
        c.getDatasetMeta(0).data.forEach((bar, k) => g.fillText(c.data.datasets[0].data[k], bar.x, bar.y - 12)); g.restore();
      } }],
    });
    signal.addEventListener("abort", () => chart.destroy()); // drawn again on the next visit
  });
</script>
```

## Mirroring a widget to the audience

Only the presenter window's `share` calls reach other windows; shares from any other window are ignored. The audience window gets the latest shared state when it opens (even late) and on every change, and `ctx.onShared` replays the latest value each time the slide is entered again. Every window starts from the same defaults, so nothing needs sharing until the presenter changes something.

```html
<div class="body" id="poll" data-interactive> <!-- the whole widget, readouts included -->
  <button type="button">Add a vote</button> <span class="count">0</span> votes
</div>
<script>
  const poll = document.getElementById("poll");
  Mako.onSlide(poll, ({ share, onShared, signal }) => {
    let votes = 0; // the same default in every window
    const render = () => { poll.querySelector(".count").textContent = String(votes); };
    poll.querySelector("button").addEventListener("click", () => {
      if (Mako.role === "audience") return; // the audience window only mirrors
      votes += 1; render(); share({ votes });
    }, { signal });
    onShared((state) => { votes = state.votes; render(); });
    render();
  });
</script>
```

- State is JSON only (objects, arrays, strings, finite numbers, booleans, `null`), at most 4 KB and 8 levels deep; the latest value wins, about 20 updates a second.
- Put `data-interactive` on the widget's whole container, not only its inputs, so clicks on its readouts don't change slide. The same goes for anything that uses the arrow keys, Space or a sideways swipe itself: it should be, or sit inside, a control or a `data-interactive` element — calling `preventDefault()` alone doesn't always stop the slide changing.
- A focused range input takes the arrow keys; that is expected. Clicking elsewhere in the widget releases them, or blur the input on `pointerup`.
- Never build business actions (approvals, orders, payments) into a deck.

## Offering a file for download

Files uploaded beside the deck with **Choose a folder** (https://makodeck.com/ai/packaging.md) can be offered from a click:

```js
document.getElementById("get-model").addEventListener("click", () => Mako.requestDownload("models/robot.blend"));
```

It only asks: the site shows its own dialog, and only in the deck's own page (never the presenter or audience window), only for a file the author allowed under **Settings** → **Files**, at exactly its path in the upload. It returns `false` when nothing was sent. Deck code can `fetch` only models (`.glb`, `.gltf`, `.obj`, `.stl`…), notebooks and code attached this way — `fetch("models/robot.glb")`; download-only kinds (`.blend`, USD, FBX…) answer 404. Thumbnails and the PDF never see attachments, so draw a fallback.

## The site's libraries

The site serves a pinned set of libraries at `_mako/lib/`, relative to the page. Use them rather than a copy of your own, and never load a library from a CDN. (A classic `<script>` that asks jsDelivr, unpkg or cdnjs for D3 7 or Chart.js 4 the usual way — `https://cdn.jsdelivr.net/npm/chart.js`, `d3@7`, any release of that major, or `https://d3js.org/d3.v7.min.js` — or jsDelivr or unpkg for roughjs 4's `bundled/rough.js`, gets the site's release of that major instead, with a note on the deck page naming it. Anything else from a CDN doesn't run, and an `import` from another site in an inline module script or import map gets a `cdn_import` warning.)

| Library | Version | Licence | How to load | Path |
|---|---|---|---|---|
| `@tailwindcss/browser` | 4.3.3 | MIT | Classic `<script src>` | `_mako/lib/@tailwindcss/browser@4.3.3/index.global.js` |
| `chart.js` | 4.5.1 | MIT | Classic `<script src>`; exposes `Chart` | `_mako/lib/chart.js@4.5.1/chart.umd.min.js` |
| `d3` | 7.9.0 | ISC | Classic `<script src>`; exposes `d3` | `_mako/lib/d3@7.9.0/d3.min.js` |
| `perfect-freehand` | 1.2.3 | MIT | ES module: `import … from "./_mako/lib/…"` | `_mako/lib/perfect-freehand@1.2.3/index.mjs` |
| `roughjs` | 4.6.6 | MIT | Classic `<script src>`; exposes `rough` | `_mako/lib/roughjs@4.6.6/rough.js` |
| `simplex-noise` | 4.0.3 | MIT | ES module: `import … from "./_mako/lib/…"` | `_mako/lib/simplex-noise@4.0.3/simplex-noise.js` |

```html
<script src="_mako/lib/d3@7.9.0/d3.min.js"></script> <!-- classic: the global d3 -->
<script type="module">
  import { createNoise2D } from "./_mako/lib/simplex-noise@4.0.3/simplex-noise.js"; // modules: a RELATIVE "./" specifier
  const noise = createNoise2D(Mako.random);
</script>
```

**Tailwind CSS** is one of them: `<script src="_mako/lib/@tailwindcss/browser@4.3.3/index.global.js"></script>` in `<head>` (Tailwind 4's browser build), with any `@theme` and other Tailwind CSS in `<style type="text/tailwindcss">`. A `<script>` that loads `@tailwindcss/browser@4` from jsDelivr or unpkg gets this copy by itself. So does one written for Tailwind 3's Play CDN (`https://cdn.tailwindcss.com`) in the page itself (not inside an `<iframe srcdoc>`), with a script the site adds before it that reads its `tailwind.config` (colours, fonts, font sizes, radii, shadows, spacing, screens, `darkMode: "class"`). Most v3 classes look the same, but `bg-opacity-*` and the like do nothing, the colours are Tailwind 4's, and the Play CDN's forms, typography and aspect-ratio plugins aren't there, so write new decks for Tailwind 4. It builds its CSS in the browser — about 280 KB per view — and needs Chrome 111, Safari 16.4 or Firefox 128 or newer; plain CSS is lighter.

A wrong path in HTML or CSS, or an `import` from another site in an inline module script, gets a warning at upload; a wrong relative `import` path, or any `import` in a `.js` file of the deck, only shows up later as a missing file, so copy paths exactly. Other libraries you need: bundle them in the deck folder (check the licence).

## Previewing locally

A plain local server has neither MakoDeck's stage, `window.Mako` nor `_mako/lib/`: the page shows every slide one after another. To preview:

- Save the site's own deck script beside `index.html` (`curl -s https://makodeck.com/ai/mako-preview.js -o deck/mako-preview.js`, or `assets/mako-preview.js` from the skill) and put `<script src="mako-preview.js"></script>` as the FIRST element of `<head>`, before every other script. It is the script the site runs first in every deck, with no site to talk to: it runs the slides and `window.Mako` as on the site, and nothing is sent anywhere. Only a few things differ: `Mako.seed` is 0 (so a seeded picture differs from the site's), `Mako.role` stays `null`, `Mako.share` mirrors to nobody, `Mako.requestDownload` returns `false`, and the `f` key and links to other sites do nothing (the site's viewer handles them).
- The tag and the file may stay in the upload: the site puts its own copy first, and a second copy returns at once. Or delete both before you zip.
- For libraries, put the SAME release at the exact path the deck uses (such as `_mako/lib/chart.js@4.5.1/chart.umd.min.js`) in a preview copy of the folder only, and delete it before zipping: a top-level `_mako/` folder is refused. Or skip previewing those slides.

## Rules the renderer enforces

- **Keep draw-ins short** — about 3 seconds at most, once per slide entry — and stop on `signal`.
- **Never block the main thread:** draw in `requestAnimationFrame` chunks. Avoid workers: only `blob:` workers run, and any `new Worker(` is flagged at upload.
- **Be ready fast:** the deck must start within about 3 seconds of loading. Don't throw at load; run slide code through `Mako.onSlide`.
- **Relative URLs only,** in `fetch`, images and imports.
- **WebGL:** name the context as a literal string in your code (`getContext("webgl")` or `"webgl2"`) so thumbnails can render it, and create it with `preserveDrawingBuffer: true` so it prints.
- **Images you draw on a canvas:** set `img.crossOrigin = "anonymous"` before `src`, or reading the canvas's pixels throws.
- **Randomness:** use `ctx.random()` or `Mako.random()` for anything that shapes the picture, so every window and the thumbnail agree. For third-party code that calls `Math.random`, set `"seedMathRandom": true` in the manifest. Pin `"seed"` to keep the same picture when the files are uploaded again as a new deck.
