Download docs/EMBED.md from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 5.28 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/docs/EMBED.md
- Command line
-
hf download hf://spaces/SharpMeow/unroll/docs/EMBED.md
-
curl -L -o EMBED.md https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/docs/EMBED.md
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.