Download lib/embed.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 7.99 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/embed.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/embed.js
-
curl -L -o embed.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/embed.js
7.99 kB
| // Embedding: ?embed=1 mode (compact chrome, click to activate, Open full viewer, fullscreen), | |
| // the Embed panel with a copyable iframe snippet, and the postMessage bridge for host pages. | |
| // The bridge mirrors window.unroll; docs/EMBED.md documents the message schema. | |
| export const SITE = 'https://sharpmeow-unroll.static.hf.space'; | |
| export const SPACE = 'https://huggingface.co/spaces/SharpMeow/unroll'; | |
| const q = new URLSearchParams(location.search); | |
| const flag = (k, d) => (q.has(k) ? !['0', 'false', 'no', 'off'].includes(q.get(k).toLowerCase()) : d); | |
| export const EMBED = { | |
| on: flag('embed', false), | |
| controls: flag('controls', true), | |
| autoplay: flag('autoplay', false), | |
| framed: window.parent !== window, | |
| }; | |
| // the iframe snippet for the current view or a clean one | |
| export function snippet({ state = true, width = '100%', height = 480, base = SITE, autoplay = false, controls = true, stateParams = {} } = {}) { | |
| const u = new URL('/index.html', base); | |
| u.searchParams.set('embed', '1'); | |
| if (state) for (const [k, v] of Object.entries(stateParams)) if (v !== null && v !== undefined && v !== '') u.searchParams.set(k, v); | |
| if (autoplay) u.searchParams.set('autoplay', '1'); | |
| if (!controls) u.searchParams.set('controls', '0'); | |
| const w = typeof width === 'number' ? `${width}` : width, h = typeof height === 'number' ? `${height}` : height; | |
| const html = `<iframe src="${u.href.replace(/&/g, '&')}" title="Unroll: a Herculaneum scroll in 3D" width="${w}" height="${h}" style="border:0;max-width:100%;aspect-ratio:auto" allow="fullscreen; clipboard-write" allowfullscreen loading="lazy"></iframe>`; | |
| return { url: u.href, html }; | |
| } | |
| export function setupEmbed({ $, getState, stateParams, onActivate }) { | |
| const root = document.documentElement; | |
| let active = !EMBED.on; | |
| const gate = $('embedGate'); | |
| const setActive = (on) => { | |
| if (!EMBED.on) return; | |
| active = on; | |
| root.classList.toggle('embed-active', on); | |
| gate.hidden = on; | |
| if (on && onActivate) onActivate(); | |
| }; | |
| if (EMBED.on) { | |
| root.classList.add('embed'); | |
| if (!EMBED.controls) root.classList.add('embed-nocontrols'); | |
| gate.hidden = false; | |
| // a click or tap (or Enter or Space) hands the pointer, touch and wheel to the scene; until then the host page scrolls as usual | |
| gate.addEventListener('click', () => { setActive(true); $('gl').focus({ preventScroll: true }); }); | |
| gate.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); gate.click(); } }); | |
| // clicking back on the host page gives scrolling back to it | |
| addEventListener('blur', () => setTimeout(() => { if (!document.hasFocus()) setActive(false); }, 0)); | |
| const full = $('embedFull'); | |
| const refresh = () => { const u = new URL(location.href); for (const k of ['embed', 'controls', 'autoplay']) u.searchParams.delete(k); const p = stateParams(); for (const [k, v] of Object.entries(p)) { if (v === null || v === undefined || v === '') u.searchParams.delete(k); else u.searchParams.set(k, v); } u.hash = ''; full.href = u.href; }; | |
| refresh(); setInterval(refresh, 1000); | |
| } | |
| // Embed panel | |
| const opt = () => ({ state: $('embedState').checked, autoplay: $('embedAutoplay').checked, controls: !$('embedNoControls').checked, stateParams: stateParams() }); | |
| // the snippet box grows to its text, so the Embed sheet is the only box that scrolls (one scroll bar, 27 Oct 2026) | |
| const fit = () => { const t = $('embedCode'); if (!t || !t.offsetParent) return; t.style.height = 'auto'; t.style.height = `${t.scrollHeight + 2}px`; }; | |
| const fill = () => { const s = snippet(opt()); $('embedCode').value = s.html; $('embedPreviewURL').href = s.url; fit(); }; | |
| addEventListener('resize', fit); document.fonts?.ready.then(fit); | |
| for (const id of ['embedState', 'embedAutoplay', 'embedNoControls']) $(id).addEventListener('change', fill); | |
| $('embedCopy').addEventListener('click', async () => { | |
| fill(); | |
| let ok = false; | |
| try { await navigator.clipboard.writeText($('embedCode').value); ok = true; } catch { $('embedCode').select(); ok = document.execCommand && document.execCommand('copy'); } | |
| $('embedCopy').textContent = ok ? 'Copied' : 'Select and copy'; | |
| setTimeout(() => { $('embedCopy').textContent = 'Copy'; }, 1600); | |
| }); | |
| return { fill, isActive: () => active, setActive, info: () => ({ embedded: EMBED.on, framed: EMBED.framed, active, controls: EMBED.controls, autoplay: EMBED.autoplay }) }; | |
| } | |
| // postMessage bridge. Host pages send | |
| // { unroll: 1, id, method: 'setLayer', args: ['xray'] } -> { unroll: 1, id, type: 'result', result } | |
| // { unroll: 1, id, method: 'subscribe', args: ['change'] } -> events as { unroll: 1, type: 'event', event, payload } | |
| // { unroll: 1, id, method: 'unsubscribe', args: ['change'] } | |
| // and receive { unroll: 1, type: 'ready', version, methods } when the scene is ready. | |
| export const PRIVATE = ['open.listLocal', 'open.removeLocal']; | |
| export function setupBridge({ api, version }) { | |
| if (window.parent === window) return null; | |
| const subs = new Map(); // event -> { id, targets: Map(source -> origin) } | |
| const reply = (src, origin, msg) => { try { src.postMessage({ unroll: 1, ...msg }, origin === 'null' ? '*' : origin); } catch { /* host went away */ } }; | |
| const resolve = (path) => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), api); | |
| addEventListener('message', async (e) => { | |
| const d = e.data; | |
| if (!d || typeof d !== 'object' || d.unroll !== 1 || typeof d.method !== 'string' || d.type) return; | |
| const args = Array.isArray(d.args) ? d.args : []; | |
| const src = e.source, origin = e.origin; | |
| if (!src) return; | |
| if (d.method === 'subscribe' || d.method === 'unsubscribe') { | |
| const ev = String(args[0] || ''); | |
| let s = subs.get(ev); | |
| if (d.method === 'subscribe') { | |
| if (!s) { | |
| const r = await api.on(ev, (payload) => { for (const [t, o] of s.targets) reply(t, o, { type: 'event', event: ev, payload }); }); | |
| if (!r.ok) { reply(src, origin, { type: 'result', id: d.id, result: r }); return; } | |
| s = { id: r.id, targets: new Map() }; subs.set(ev, s); | |
| } | |
| s.targets.set(src, origin); | |
| reply(src, origin, { type: 'result', id: d.id, result: { ok: true, event: ev } }); | |
| } else { | |
| if (s) { s.targets.delete(src); if (!s.targets.size) { await api.off(s.id); subs.delete(ev); } } | |
| reply(src, origin, { type: 'result', id: d.id, result: { ok: true, event: ev } }); | |
| } | |
| return; | |
| } | |
| // a host page must not read or delete what this visitor keeps in their own browser | |
| if (PRIVATE.includes(d.method)) { reply(src, origin, { type: 'result', id: d.id, result: { ok: false, error: `${d.method} is not available to host pages: it touches scrolls this visitor keeps in their own browser.`, code: 'forbidden' } }); return; } | |
| if (['on', 'off', 'register'].includes(d.method)) { reply(src, origin, { type: 'result', id: d.id, result: { ok: false, error: `${d.method} is not available over postMessage; use subscribe and unsubscribe.`, code: 'invalid' } }); return; } | |
| const fn = resolve(d.method); | |
| if (typeof fn !== 'function') { reply(src, origin, { type: 'result', id: d.id, result: { ok: false, error: `Unknown method "${d.method}". Ask for schema to list them.`, code: 'invalid' } }); return; } | |
| let result; | |
| try { result = await fn(...args); } catch (err) { result = { ok: false, error: String(err && err.message || err), code: 'failed' }; } | |
| if (d.method === 'listScrolls' && result && Array.isArray(result.scrolls)) result = { ...result, scrolls: result.scrolls.filter((x) => !x.local) }; | |
| reply(src, origin, { type: 'result', id: d.id, result: JSON.parse(JSON.stringify(result)) }); | |
| }); | |
| const announce = async () => { const v = await version(); const sch = await api.schema(); reply(window.parent, '*', { type: 'ready', version: v, methods: Object.keys(sch.methods) }); }; | |
| return { announce }; | |
| } | |