# Coded decks

Already have a deck written in HTML? Or want your AI to write one?
Upload it, and it becomes a live deck: it follows your arrow keys, phones
follow it, and it can draw the room's answers in its own style.

## The convention

One HTML file. One element per slide, in order, with `data-slide`.

```html
<section data-slide data-title="Hello">
  <h1>Hello</h1>
</section>

<section data-slide data-id="poll"
         data-ask='{"id":"poll","type":"choice","prompt":"Tea or coffee?","options":["Tea","Coffee"]}'>
  <h2>Tea or coffee?</h2>
  <div data-results="poll"></div>
  <p data-step="1">One more thing.</p>
  <aside class="notes">Wait for the bars to settle.</aside>
</section>
```

That is all. Upload it from the app (drop it on the page) and it works.

A complete example that uses every convention: [the example deck](/kit/example-deck).
Open it, view the source, or save it and drop it into the app.

### Per slide

| Attribute | Does |
| --- | --- |
| `data-title` | The slide's name. Otherwise its first `h1` or `h2`. |
| `data-notes` | Speaker notes. Or an `<aside class="notes">` inside. |
| `data-id` | A stable identity. Upload a new version and your settings stay. |
| `data-steps` | How many extra beats. Or mark elements `data-step="1"`, `"2"`... |
| `data-ask` | A question, as JSON. Phones are asked it on this slide. Give it an `id` to show its answers elsewhere. |
| `data-phone` | Any other phone view, as JSON. |

Elements with `data-step` light up one per arrow press, with the house
"illumination" fade. Restyle `.is-lit` to change that.

### Live answers in your own design

```html
<b data-live="poll.n"></b> answered
<b data-live="people"></b> in the room
<div data-results="poll"></div>
```

`data-live` takes `n`, `avg`, `median`, `total`, `top` or `people`.
`data-results` draws plain bars: style `.stoa-row`, `.stoa-fill`, `.stoa-label`, `.stoa-n`, `.stoa-lead`.

Or, in script:

```html
<script>
  Stage.on('data', (d) => {
    const poll = d.aggs.poll;           // { n, counts: { o1: 12, o2: 7 }, total }
    const room = d.people;
  });
  Stage.on('goto', ({ index, step }) => { /* your own transitions */ });
</script>
```

### What each question sends

`d.aggs` has one entry per question id, the same the big screen draws.
`d.interactions[id]` is the question itself: `type`, `prompt`, `options`,
`view`. `d.people` is how many have joined. A coded deck hears this on the
big screen. A custom slide hears it on the big screen and in the console,
the same in both. The editor's preview has no live answers.

| Question | `d.aggs[id]` |
| --- | --- |
| `choice` | `{ n, counts: { optionId: number }, total }`. `n` people, `total` picks. |
| `scale` | `{ n, avg, dist: { value: number }, min, max, width }`. A wide scale counts in buckets `width` wide. |
| `text` | `{ n, words: [[word, count]], items: [{ id, text, at }] }`. Most said first; the latest items. |
| `qa` | `{ n, items: [{ id, text, votes, answered, at }] }`. Unanswered first, then by votes. |
| `number` | `{ n, avg, median, min, max, hist: [{ from, to, n }] }`. With `correct`: also `correct` and `closest: [{ name, value, off }]`. |
| `photo`, `audio` | `{ n, items: [{ id, url, at }] }` |
| `form` | `{ n }` only. What people filled in stays with you. |
| `custom` | `{ n, latest }`: the latest 50 values sent with `Room.send`. |
| `@leaderboard` | `{ n, rows: [{ name, score }] }`: the top quiz scores. |

Only approved answers arrive. A filtered answer, a custom one too, arrives
once you show it. Names arrive only where the room shows them:
the leaderboard, the closest guesses, and a Q&A with "show who asked" on
(`name` on each item). A name the room keeps hidden reads "Guest 12". Draw
the empty case too: `n` can be 0.

## The library kit: three.js, GSAP and more

Your HTML can import a set of animation and graphics libraries, bundled and
served by {{name}} itself, so a venue that blocks CDNs still runs them. Import
them by name:

```html
<script type="module">
  import * as THREE from 'three';
  import { gsap } from 'gsap';
  import confetti from 'canvas-confetti';
</script>
```

In the kit:

- `three`, `three/webgpu`, `three/tsl`, and every three.js add-on by its path,
  as in three's own examples: `three/addons/controls/OrbitControls.js`,
  `three/addons/loaders/GLTFLoader.js` (and DRACO), `three/addons/postprocessing/EffectComposer.js`,
  `UnrealBloomPass`, `Water`, `Sky` and the rest.
- `gsap`, with every plugin: `gsap/ScrollTrigger`, `gsap/Flip`, `gsap/SplitText`,
  `gsap/MorphSVGPlugin`, `gsap/DrawSVGPlugin`, `gsap/MotionPathPlugin`, `gsap/Draggable`,
  `gsap/CustomEase` ... or all at once from `gsap/all`.
- `motion` (the engine behind Framer Motion), and `motion/react` (also as
  `framer-motion`) for `<motion.div>`, AnimatePresence and layout animations.
- `react`, `react-dom`, `react-dom/client`, `react/jsx-runtime`, and `htm/react`
  to write JSX without a build step: one copy of React for all of them.
- `animejs`, `d3`, `pixi.js`, `p5`, `lottie-web`, `matter-js`, `canvas-confetti`,
  `chart.js`, `simplex-noise`, `tone`.

The always current list, with versions, is in [the catalog](/api/v1/catalog)
under `kit`. Each library loads only when a deck imports it.

An import map is added when {{name}} serves your file, so the bare names resolve
(a map of your own is merged in, and wins). On the big screen you may also
load other libraries from a CDN. On phones you may not (see the sealed tier
below): use the kit, or add the file to your deck. Either way, phones and the
big screen download the kit once and keep it, slide after slide.

## Two places for your own code

Besides a whole coded deck, a single slide of a normal deck can be your HTML,
and so can a single phone screen:

- **A custom stage slide** (the big screen): in the editor, add a slide and
  pick **Custom (HTML)** under Show. Or for an agent,
  `"stage": { "type": "custom", "html": "..." }`.
- **A custom phone screen** (every phone): on a slide, under Phones, pick
  **Your own (HTML)**. Or `"phone": { "type": "custom", "html": "...",
  "interaction": "pick", "fill": true }`, where the optional `interaction`
  is a question of type `custom` (`"pick": { "type": "custom" }`) that keeps
  what `Room.send` sends. A screen can also be a page from an uploaded
  folder: `"source": "src_x", "src": "screen.html"`, so it can load models,
  textures and sound by relative path.

Both run in a sandbox with no origin of their own, and the kit's import map is
added, so three.js, GSAP and the rest work the same there.

## The Stage bridge (custom slides and coded decks)

`Stage` exists inside the app; opened on its own, it is simply absent, so a
deck still works in a plain browser. On top of what is above:

```js
Stage.on('step', ({ step, dir, instant }) => { /* dir is 1 or -1; instant: a jump */ });
Stage.on('in',  () => { /* this deck or slide is on screen */ });
Stage.on('out', () => { /* it left: pause your loop */ });
Stage.on('progress', ({ value }) => { /* 0 to 1 through the deck, moving smoothly */ });
Stage.on('signal', ({ name, data, from }) => { /* a live cue from a phone */ });
Stage.on('reaction', (counts) => { /* the reaction taps */ });
Stage.broadcast('burst', { color: '#f0a' });   // a live cue to every phone
Stage.timeline(tl, 'deck');                     // a GSAP timeline, played by the beats
Stage.transition(async ({ fromEl, toEl, dir }) => { /* move between pages yourself */ });
Stage.motion('zoom', fromEl, toEl);             // play any transition between two elements
```

Beats build the same as a coded deck: put `data-step="1"`, `data-step="2"` on
elements (a custom slide counts them for you), and add
`data-anim="rise|fall|left|right|zoom|pop|fade|blur"` for a named entrance.
The first beat's pieces arrive with their entrance when the page does.

**Timelines.** `Stage.timeline(tl, which)` plays a GSAP timeline by the beats:
one press forward plays on to the next stop, back plays back, a jump goes
straight there. `which` is `'deck'` (every beat of every page is a stop: one
timeline for a whole world), one page (its number, its `data-id`, or an
element in it), or nothing (the only page, else the deck). Stops are the
timeline's labels (`"2"` for a page's beat 2; `"3.1"` or `"<data-id>.1"` across a
deck), or spread evenly when it has none.

**Progress.** `Stage.on('progress')` gives where the talk is through the deck,
0 to 1, and moves smoothly from beat to beat. Tie a camera to a path with it
and every press flies there.

## Moving between pages

A coded deck's pages move with the same transitions as the app's slides:

- `data-transition="zoom"` on a page, for how it arrives. Any built-in name
  (fade, slide, rise, zoom, blur, iris, wipe, curtain, flip, spin, drop,
  glide, morph, none), one of the deck's own designs, or a design written in
  place as JSON: `data-transition='{"base":"slide","duration":600}'`.
- `<html data-transition="...">` for the whole deck.
- What you set on the slide in the app wins over both.
- **Magic move:** give a piece the same `data-morph="name"` on two pages and
  it glides from one place, size and colour to the other while the rest
  dissolves. No code.
- **Anything at all:** `Stage.transition(fn)` hands you both pages (on screen
  together, the old one held where it was) and waits for you, up to 8 seconds.
  Use GSAP, three.js, your own canvas. Return `false` to let the bridge play
  its own.

[The magic move example](/kit/examples/magic) shows the first two.

## A deck that is one world

A three.js scene the camera flies through, one long canvas: there are no
pages to show and hide, so list the slides instead, and draw each one
yourself:

```html
<script type="application/json" id="joocy-slides">
[
  { "id": "arrive", "title": "Arrive", "notes": "Let the stars settle." },
  { "id": "field", "title": "A field of light", "steps": 2 },
  { "id": "vote", "title": "Which world next?",
    "ask": { "id": "world", "type": "choice", "prompt": "Which world next?", "options": ["Ice", "Fire", "Ocean"] } }
]
</script>
<script type="module">
  import * as THREE from 'three';
  // ... the scene ...
  Stage.on('progress', ({ value }) => camera.position.copy(path.getPointAt(value)));
  Stage.on('goto', ({ key, step }) => { /* captions, the second beat of "field" ... */ });
</script>
```

Each entry can carry `title`, `steps`, `notes`, `ask`, `phone` and `thumb`,
like a page's attributes. {{name}} reads the list without running anything, so
your AI can upload a world in one call. [The world example](/kit/examples/world)
is a whole one: a camera path between five places, phones lighting stars in
it, a question's answers growing as pillars of light.

## The big screen around your deck

- **The join badge** sits in the bottom left corner on slides that ask
  something: about a fifth of the width, a sixth of the height. Keep that
  corner calm. (Room settings: "Join code in the corner".)
- **Reactions** rise up the right edge, across 22% of the width. Keep words
  people must read out of that strip.
- **Short notices** ("Answers locked", "Reconnecting") flash at the top centre.
- **The mouse is the show's, unless you turn it on.** Out of the box,
  nothing of the mouse reaches a coded slide, and a click on the screen's
  left or right edge moves the show. Turn on "Coded slides take the mouse"
  in the room settings (`"settings": { "mouse": true }`), and clicks,
  hovers, drags, a trackpad and a pen all reach your deck, the edges too.
  Any coded slide can say otherwise under Screen, "Takes the mouse"
  (`stage.mouse`).
- **A link replaces your deck.** With the mouse on, a plain link opens inside
  your deck's frame, and that page stays on the big screen until it reloads.
  It hears no answers and gets no pointer or keys. A new window
  (`target="_blank"`, `window.open`) or a download does nothing. Put links on
  phones instead (`Room.open`, or a card's button).
- **Keys reach your deck once someone clicks into it**, so only with the
  mouse on. The bridge still
  hands the presenter's keys to the show: arrows, Space, Enter, Page Up,
  Page Down, Backspace, Home, End, Escape, and `B` `.` `J` `R` `L` `N` `O` `F`
  `M` `C` `?`. Every other key is yours: digits, the other letters, and keys
  with Ctrl, Cmd or Alt. Leave `G` and `H` alone too: on the big screen they
  open all slides and the keys help.
- **What those keys do:** `R` reveals results, `J` shows the big join code,
  `L` locks answers, `N` shows notes, `B` goes black, `O` shows all slides,
  `F` full screen, `M` sound, `C` the console.
- **Stickers** are pictures you can use: `/stickers/<name>.big.webp` (320 px,
  animated, for the big screen), `/stickers/<name>.webp` (128 px) and
  `/stickers/<name>.still.webp`. The names are in the catalog under `glyphs`.

## Trying a deck on your own computer

Add these three lines to its `<head>`, and it runs in any browser, the arrow
keys, the library kit and the transitions included. Inside the app they are
already there, and these do nothing.

```html
<script src="https://YOUR-SITE/kit/map.js"></script>
<script src="https://YOUR-SITE/kit/motion.js"></script>
<script src="https://YOUR-SITE/kit/bridge.js"></script>
```

Opened on its own, no answers arrive: try your drawing code on sample
answers in the shapes above. Without these scripts there is no `Stage` at
all, so guard it: `window.Stage && Stage.on(...)`.

Testing on the real big screen: its link opens on a **Start** button,
because browsers play sound only after a click. A browser test must click
Start first. Join from a phone to see live answers and `Room.me` arrive.

## The Room bridge (phone screens)

A custom phone screen talks through `Room`:

```js
Room.send({ pick: 'a' });              // save an answer into the slide's question
Room.emit('spark', { tag: Room.me.tag }); // a live cue to the big screen
Room.on('signal', ({ name, data }) => { /* a cue from the big screen */ });
Room.on('data', (agg) => { /* the room's answers, when phones may see them */ });
Room.on('state', ({ me }) => { /* the beat moved, or Room.me changed */ });
Room.vibrate(12);                      // a short buzz
Room.open('https://...');              // offer a link; the phone confirms it first
hello.textContent = `Hi, ${Room.me.name || 'friend'}`;
```

An answer sent with `Room.send` that has a bad word in it, anywhere, keys
too, is filtered: it waits for your OK, like a filtered word on a wall. The
phone is told it was sent. It shows in the console as "Filtered: needs your
OK", and reaches the big screen and other phones once you show it. The
question's own switch, "Filter bad words", is under the phone screen in the
editor.

`Room.me.tag` is a short stable id for this person, the same one the big screen
sees on their signals, so a deck can give each phone its own colour.

`Room.me.name` is their own name, as the room shows it. It is empty until they
give one, and while the room keeps it hidden. A screen never hears anybody
else's name.

`Room.on('data')` gives the slide's question in the shapes above, with fewer
items, when phones may see results. `Room.theme` and the CSS variables
`var(--accent)`, `var(--ink)`, `var(--display)` carry the deck's look.

A tap is only ever a tap: a quick second tap never zooms the phone, so a
screen can be one big button people hit as fast as they like. Pinching still
zooms. An element with a drag of its own, like a drawing pad, sets
`touch-action: none` on itself.

## The sealed tier: what phone screens may reach

A phone screen is other people's device, so it is sealed. It may load the kit,
your deck's own files and a font service, and nothing else: no fetch, picture,
socket or form reaches another site, and it cannot leave the page or read the
page around it. So whatever it draws, anything typed into it can go only to the
room, through `Room.send` and `Room.emit`. {{name}} refuses, and never shows, a
phone screen that asks for a password, a card number or a one-time code, and
refuses any answer that is a card number. Do not ask a room for secrets; it
will not work.

So on a phone screen there are no downloads, no popups, no dialogs and no
`localStorage`. To offer a link, use `Room.open`. To let people keep a picture,
show it as an `<img>`: they long-press it to save it.

A custom stage slide on the big screen (your own machine) is not sealed: load
any library from any CDN there.

## Transitions between slides

Decks change slides with a move you choose: fade (the default), none (a
cut), slide (a push), rise, zoom, blur, iris, wipe, curtain, flip, spin, drop,
glide, or morph (magic move: the title, the number, the picture glide into
their new places). Set a default for the deck under **Room**, or give any
slide its own under **Screen**, with a duration, and press **Play it** to see it.

You, or your AI, can also design transitions of your own: what the leaving
and arriving slides do, which pieces arrive one by one, what glides, how
phones arrive. They are data, checked when saved, so anything can be
designed and nothing in one can run code or load anything.
[The deck format](DECK-FORMAT.md) has the details; your AI reads them in the
guide, and its `get_transitions` tool hands it every built-in to start from.

## Live signals, during a talk

A phone screen's `Room.emit(name, data)` reaches a custom slide or coded deck on
the big screen as `Stage.on('signal')`: taps, drags, a colour, a game move. The
big screen's `Stage.broadcast(name, data)` reaches every phone screen as
`Room.on('signal')`. Both are kept nowhere and are rate limited; a phone's
signals are word filtered, so the audience cannot push a bad word onto the
screen. See [the showcase deck](/kit/examples/showcase) for a 3D scene the
room's taps light up.

## Keynote's HTML export

In Keynote: **File**, **Export To**, **HTML**. Drop the folder (or a zip of
it). {{name}} recognises it and plays it with Apple's own player, every build and
transition as designed. Titles come from each slide's largest words, one step
per click, and Keynote's picture of each slide becomes its thumbnail. Upload a
new export later and your questions and notes stay on their slides.

{{name}} drives Apple's player directly. Next plays the next build or transition
exactly as designed. Back, a thumbnail or a jump fades for a moment and lands
on the whole slide: Apple's player draws a slide whole only by playing into
it, so {{name}} plays that lead in, very fast, out of sight. A thumbnail lands at
the start of its slide, so its builds can play again. Presses that arrive
while it animates are kept, never lost or doubled.

An agent can do the same: upload every file with its path, then apply with
`"scan": true`.

## Other shapes that just work

- `section.slide`, or plain `section`s under `body` or `main`.
- **reveal.js** decks, as they are.
- A deck that drives itself in script can say so:

```html
<script>
  window.StageDeck = {
    slides: () => SLIDES.map((s) => ({ title: s.name, notes: s.note, steps: s.beats })),
    goto: (index, step) => showSlide(index, step),
  };
</script>
```

The app runs it once in a hidden sandbox to read its slides.

## Many files

Put the HTML, styles, scripts, fonts and pictures in one folder, with the
HTML named `index.html`. Zip the folder. Drop the zip.
Relative links between the files work as they did on your computer.

## Uploading a new version

Editor, select one of its slides, **Screen**, **Upload a new version**.
Slides you set up (questions, notes, phone views) keep everything.
Pages that are gone are removed. New pages appear in place.
Native slides you added between its pages stay where they were.

That cuts both ways. A slide that already has a phone view, notes or a
title keeps them, even if the new file says something else in `data-phone`,
`data-notes` or its slide list. A page's question changes only when the
page's own question does. To change those from code, write them into the
deck after uploading: your AI patches each slide's `phone`, `phoneByStep` and
`notes`.

Your AI can do the same in two calls; see the [AI guide](/api/v1/guide).

## How it is kept safe

Your HTML runs in a sandbox with no address of its own. It cannot read
anybody's sign-in, cannot call the app's API as you, and cannot touch the
page around it. It talks to the app only through the small bridge script,
which the app adds when it serves the file. You never need to add it.

On the big screen (your own machine) that sandbox is open inside: load any
library, reach any site. It opens no new windows and saves no files. A coded
slide takes the mouse and pen only if you turn that on, and the app's own
controls stay above it, out of its reach. A phone screen is sealed
tighter, because it runs on other people's devices: it may reach only the
library kit, your deck's own files and a font service, so anything typed into
it can go nowhere but the room. Either way, the app's own pages are
untouched, and a page opened on its own address runs nothing and only
explains itself.

## Rules of thumb

- Size things with viewport units or percentages: the stage is any size.
- Do not rely on `localStorage`; the sandbox has none.
- Arrow keys and the presenter's keys belong to the presenter. The bridge
  passes them up (see The big screen around your deck).
- Keep the bottom left corner and the right edge calm: the join badge and
  the reactions live there.
- Check it in a browser first: opened on its own, the arrow keys still work.
