# Embedding Unroll

Unroll can live inside any web page as an iframe, fully interactive: visitors can unroll, scrub,
switch layers, open close-ups, use Destroy and every other tool right in the frame.

## The snippet

```html
<iframe src="https://sharpmeow-unroll.static.hf.space/index.html?embed=1"
  title="Unroll: a Herculaneum scroll in 3D" width="100%" height="480"
  style="border:0;max-width:100%" allow="fullscreen; clipboard-write" allowfullscreen loading="lazy"></iframe>
```

The **</>** button in the viewer builds this for the current view, with options for autoplay and hidden controls,
and copies it. `window.unroll.embed.snippet(options)` returns the same `{ url, html }`.
oEmbed consumers can read [`/oembed.json`](../oembed.json); the page links it with `rel="alternate"`.

Use the direct static URL (`sharpmeow-unroll.static.hf.space`), not the huggingface.co Space page. The static host
sends no `X-Frame-Options` or `frame-ancestors` header, so any site can frame it.

## Sizing

The viewer fills its frame and follows it when it resizes. For a responsive embed, give the frame a box and let it fill it:

```html
<div style="position:relative;width:100%;aspect-ratio:16/10">
  <iframe src="https://sharpmeow-unroll.static.hf.space/index.html?embed=1" title="Unroll"
    style="position:absolute;inset:0;width:100%;height:100%;border:0"
    allow="fullscreen; clipboard-write" allowfullscreen loading="lazy"></iframe>
</div>
```

The layout gets more compact as the frame gets smaller; below 420 px of height the title is hidden.

## URL parameters

| Parameter | Meaning |
| --- | --- |
| `embed=1` | Embed layout: compact header, an **Open full viewer** link, a fullscreen button, click to interact |
| `autoplay=1` | Play the unroll once the scroll is loaded |
| `controls=0` | Hide the controls (the scene stays interactive) |
| `scroll`, `t`, `layer`, `focus`, `view`, `compare`, `panel`, `destroy`, `tool`, `sandbox`, `cam` | Start state, as in the full viewer (see [AGENTS.md](../AGENTS.md)) |

Example: `index.html?embed=1&autoplay=1&layer=overlay&t=0.4`.

## Click to interact

Until the visitor clicks (or taps, or presses Enter on) the "Click to interact" layer, the mouse wheel and touch
scrolling go to the host page, so the frame never traps a reader scrolling past it. After the click the scene takes
drag, pinch and wheel. Clicking anywhere on the host page hands them back. The toolbar, scrubber and chips work
without activating. `embed.activate(true|false)` does the same from code, and `getState().embed.active` reports it.

## postMessage API

Host pages drive the viewer with `postMessage`. It mirrors `window.unroll`: every method in [`/api.json`](../api.json)
is available by its name, with the same arguments and the same `{ ok, ... }` results.

Send to the frame:

```js
frame.contentWindow.postMessage({ unroll: 1, id: 7, method: 'setLayer', args: ['xray'] }, 'https://sharpmeow-unroll.static.hf.space');
```

| Message from the host | Fields |
| --- | --- |
| Call a method | `{ unroll: 1, id, method, args }`. `method` is an API name such as `getState`, `setUnroll`, `camera.orbit`, `destroy.tear`, `bench.run`; `args` is an array |
| Subscribe to an event | `{ unroll: 1, id, method: 'subscribe', args: [eventName] }` |
| Unsubscribe | `{ unroll: 1, id, method: 'unsubscribe', args: [eventName] }` |

| Message from the viewer | Fields |
| --- | --- |
| Ready | `{ unroll: 1, type: 'ready', version: { version, date, sha, api }, methods: [names] }`, sent when the scroll is loaded |
| Result | `{ unroll: 1, type: 'result', id, result }`. `result` is `{ ok: true, ... }` or `{ ok: false, error, code }` |
| Event | `{ unroll: 1, type: 'event', event, payload }` |

Events: `change`, `unroll`, `layer`, `focus`, `panel`, `destroy`, `camera`, `settled`, `scroll`, `extension`, `error`.

Not available over postMessage: `on`, `off` and `register` (use subscribe), and `open.listLocal` and `open.removeLocal`,
which would let a host read or delete scrolls the visitor keeps in their own browser (code `forbidden`).
`listScrolls` leaves out the visitor's local scrolls. Check `event.origin` on your side as usual.

A small client:

```js
const frame = document.querySelector('iframe');
const ORIGIN = 'https://sharpmeow-unroll.static.hf.space';
let seq = 0; const pending = {};
const unroll = (method, ...args) => new Promise((res) => {
  const id = ++seq; pending[id] = res;
  frame.contentWindow.postMessage({ unroll: 1, id, method, args }, ORIGIN);
});
addEventListener('message', (e) => {
  if (e.origin !== ORIGIN || e.data?.unroll !== 1) return;
  if (e.data.type === 'ready') { unroll('subscribe', 'layer'); unroll('setUnroll', 0.5); }
  if (e.data.type === 'result') { pending[e.data.id]?.(e.data.result); delete pending[e.data.id]; }
  if (e.data.type === 'event') console.log(e.data.event, e.data.payload);
});
```

## Test page

[`tests/embed-host.html`](../tests/embed-host.html) frames the viewer next to buttons that drive it with postMessage.
`?src=` points it at another viewer URL. `node tests/embed.mjs <viewer URL> <host URL>` serves both on different
origins and checks click to interact, wheel handling, drag, controls, fullscreen, resizing, autoplay, `controls=0`
and the postMessage API in headless Chrome.
