unroll / docs /EMBED.md
SharpMeow's picture
Public API at window.unroll (schema, help, coverage test; ui and bench groups), embed mode with postMessage bridge and oEmbed, version 0.9.4 in About and API, ink_9um (9 µm) label and credit, Ink Bench hardening from the audit, hashchange without reload, capped test hooks
cc6b6d8
|
Raw History Blame Contribute Delete
5.28 kB

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

<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; 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:

<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)

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 is available by its name, with the same arguments and the same { ok, ... } results.

Send to the frame:

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:

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 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.