unroll / api.json
SharpMeow's picture
One page scroll bar and American English
757768f verified
Raw History Blame Contribute Delete
51.1 kB
{
"name": "Unroll",
"app": {
"version": "1.7.0",
"date": "2026-10-24",
"sha": "e5067cd"
},
"apiVersion": "1.0.0",
"docs": {
"llms": "/llms.txt",
"agents": "/AGENTS.md",
"addingAScroll": "/docs/ADDING_A_SCROLL.md",
"changelog": "/CHANGELOG.md",
"embed": "/docs/EMBED.md",
"oembed": "/oembed.json"
},
"global": "window.unroll",
"conventions": [
"Every method returns a Promise of { ok: true, ... } or { ok: false, error, code }. Nothing throws.",
"Error codes: invalid (bad arguments), not_found, unavailable (feature missing in this build or scroll), timeout, conflict, failed, load_failed.",
"Screen coordinates are CSS pixels of the viewport; scene coordinates are millimeters.",
"Add-ons register more groups with unroll.register(group, methods, specs), for example unroll.bench. They appear in unroll.schema() with extension: true.",
"Every interactive element has data-testid and data-api naming its API equivalent."
],
"embed": {
"url": "https://sharpmeow-unroll.static.hf.space/index.html?embed=1",
"params": {
"embed": "1: compact layout, click to interact",
"autoplay": "1: play on load",
"controls": "0: hide the controls"
},
"iframeAllow": "fullscreen; clipboard-write"
},
"postMessage": {
"description": "Host pages drive an embedded viewer with postMessage. Every method above is callable by name; on, off and register are replaced by subscribe and unsubscribe.",
"toViewer": {
"call": {
"unroll": 1,
"id": "number|string",
"method": "string (an API method name)",
"args": "array"
},
"subscribe": {
"unroll": 1,
"id": "number|string",
"method": "subscribe",
"args": [
"eventName"
]
},
"unsubscribe": {
"unroll": 1,
"id": "number|string",
"method": "unsubscribe",
"args": [
"eventName"
]
}
},
"fromViewer": {
"ready": {
"unroll": 1,
"type": "ready",
"version": "{ version, date, sha, api }",
"methods": "array of method names"
},
"result": {
"unroll": 1,
"type": "result",
"id": "the id you sent",
"result": "{ ok: true, ... } | { ok: false, error, code }"
},
"event": {
"unroll": 1,
"type": "event",
"event": "string",
"payload": "object"
}
},
"forbidden": [
"open.listLocal",
"open.removeLocal"
]
},
"methods": {
"version": {
"group": "meta",
"summary": "The app version from version.json: { version (semver), date, sha, api }. Also in api.json and Bench export bundles.",
"call": "window.unroll.version()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"help": {
"group": "meta",
"summary": "A short guide: how calls work, a quick start and the methods by group. help(\"destroy\") narrows to a group, help(\"setLayer\") to one method.",
"call": "window.unroll.help(topic?)",
"params": [
{
"name": "topic",
"type": [
"string"
],
"required": false,
"description": "A method or group name"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"schema": {
"group": "meta",
"summary": "This API: version, every method with its parameters, events, URL parameters and keyboard actions.",
"call": "window.unroll.schema()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"getState": {
"group": "meta",
"summary": "The whole page state as JSON: scroll, unroll position, layer, winding, close-up, panels, Destroy mode, camera, readout and a text description of the view.",
"call": "window.unroll.getState()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"describe": {
"group": "meta",
"summary": "A plain-text description of what is on screen now (the same text screen readers get).",
"call": "window.unroll.describe()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"links": {
"group": "meta",
"summary": "Every link on the page (credits, licenses, docs) with its text and URL.",
"call": "window.unroll.links()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"deepLink": {
"group": "meta",
"summary": "A URL that restores the current state (scroll, t, layer, focus, compare, panel, Destroy mode; camera too if asked).",
"call": "window.unroll.deepLink(camera?)",
"params": [
{
"name": "camera",
"type": [
"boolean"
],
"required": false,
"description": "Include the camera position (cam=...)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"on": {
"group": "meta",
"summary": "Subscribe to an event; the callback gets a JSON payload. Returns { ok, id } for off().",
"call": "window.unroll.on(event, callback)",
"params": [
{
"name": "event",
"type": [
"string"
],
"required": true,
"description": "Event name",
"enum": [
"change",
"unroll",
"layer",
"focus",
"panel",
"destroy",
"camera",
"settled",
"scroll",
"extension",
"error"
]
},
{
"name": "callback",
"type": [
"function"
],
"required": true,
"description": "Called with (payload)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"off": {
"group": "meta",
"summary": "Remove a subscription made with on().",
"call": "window.unroll.off(id)",
"params": [
{
"name": "id",
"type": [
"number"
],
"required": true,
"description": "The id on() returned"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"run": {
"group": "meta",
"summary": "Run a batch of calls in order. Each step is [\"method\", ...args] or { \"method\", \"args\" }; dotted names reach groups (e.g. \"camera.orbit\"). Stops at the first failure unless options.continueOnError.",
"call": "window.unroll.run(steps, options?)",
"params": [
{
"name": "steps",
"type": [
"array"
],
"required": true,
"description": "The calls"
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ continueOnError: boolean }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"waitFor": {
"group": "meta",
"summary": "Wait until the state matches. condition is \"ready\", \"settled\" (unroll and camera at rest), \"overlayLoaded\", or an object of dotted state paths to values or to { eq, ne, gte, lte, gt, lt, in }.",
"call": "window.unroll.waitFor(condition, options?)",
"params": [
{
"name": "condition",
"type": [
"string",
"object"
],
"required": true,
"description": "What to wait for"
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ timeout: ms (default 10000) }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"reload": {
"group": "meta",
"summary": "Reload the page (keeps the URL state).",
"call": "window.unroll.reload()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"listScrolls": {
"group": "scrolls",
"summary": "Scrolls in the registry and scrolls opened in this browser (local).",
"call": "window.unroll.listScrolls()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"openScroll": {
"group": "scrolls",
"summary": "Open a scroll by registry id or local key. The page reloads; call waitFor(\"ready\") after the new page loads.",
"call": "window.unroll.openScroll(id)",
"params": [
{
"name": "id",
"type": [
"string"
],
"required": true,
"description": "Registry id or local key (local-...)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"setUnroll": {
"group": "view",
"summary": "Set the unroll position, 0 rolled to 1 fully unrolled. Animates unless instant.",
"call": "window.unroll.setUnroll(t, options?)",
"params": [
{
"name": "t",
"type": [
"number"
],
"required": true,
"description": "Position",
"minimum": 0,
"maximum": 1
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ instant: boolean }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"play": {
"group": "view",
"summary": "Start or stop the automatic unroll (toggles if omitted).",
"call": "window.unroll.play(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "Play or pause"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"setLayer": {
"group": "view",
"summary": "Show a layer: \"xray\", \"ink\" or \"overlay\" (aliases \"ct\", \"gpu\", or the manifest layer id).",
"call": "window.unroll.setLayer(id)",
"params": [
{
"name": "id",
"type": [
"string"
],
"required": true,
"description": "Layer"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"focus": {
"group": "view",
"summary": "Open the close-up of a winding (current winding if omitted), or close it with null. options.view picks the tab: \"main\", \"overlay\" or \"compare\".",
"call": "window.unroll.focus(winding?, options?)",
"params": [
{
"name": "winding",
"type": [
"string",
"number",
"null"
],
"required": false,
"description": "Winding id such as \"w059\" (or its number, 59), null to close"
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ view: \"main\" | \"overlay\" | \"compare\" }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"compare": {
"group": "view",
"summary": "Compare swipe in the close-up: on/off and the split position 0..1 (0 shows only the overlay, 1 only the main ink). Opens the close-up on the current winding if needed.",
"call": "window.unroll.compare(on, pos?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": true,
"description": "Show the compare view"
},
{
"name": "pos",
"type": [
"number"
],
"required": false,
"description": "Split position",
"minimum": 0,
"maximum": 1
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"closeup": {
"group": "view",
"summary": "Zoom and pan the close-up image: { scale } multiplies the zoom, { x, y } pans in CSS px, { fit: true } fits it.",
"call": "window.unroll.closeup(options)",
"params": [
{
"name": "options",
"type": [
"object"
],
"required": true,
"description": "{ scale, x, y, fit }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"panel": {
"group": "view",
"summary": "Open or close a panel: \"about\", \"help\", \"open\" (Open your own), \"why\" (why two ink layers), \"embed\" (the iframe snippet). Toggles if open is omitted.",
"call": "window.unroll.panel(name, open?)",
"params": [
{
"name": "name",
"type": [
"string"
],
"required": true,
"description": "Panel",
"enum": [
"about",
"help",
"open",
"why",
"embed"
]
},
{
"name": "open",
"type": [
"boolean"
],
"required": false,
"description": "Open or close"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"sound": {
"group": "view",
"summary": "Crackle sound for Destroy on or off (browsers need a user gesture first to play audio).",
"call": "window.unroll.sound(on)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": true,
"description": "Sound on"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"fullscreen": {
"group": "view",
"summary": "Enter or leave full screen (browsers may refuse without a user gesture).",
"call": "window.unroll.fullscreen(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "Full screen"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"screenshot": {
"group": "view",
"summary": "A PNG of the view as a data URL. options.clean leaves out the title overlay; options.download also saves it like the camera button.",
"call": "window.unroll.screenshot(options?)",
"params": [
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ clean: boolean, download: boolean }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"key": {
"group": "view",
"summary": "Press a keyboard shortcut, as a person would (see schema().keys).",
"call": "window.unroll.key(key, options?)",
"params": [
{
"name": "key",
"type": [
"string"
],
"required": true,
"description": "Key name, e.g. \"t\", \"ArrowRight\", \"?\""
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ shift: boolean }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"reset": {
"group": "view",
"summary": "Back to the start: close panels, leave Destroy, put every sheet back, roll up, reset the camera and layer.",
"call": "window.unroll.reset()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"camera.get": {
"group": "camera",
"summary": "Camera position, target, distance, azimuth and polar angle (degrees), field of view.",
"call": "window.unroll.camera.get()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"camera.orbit": {
"group": "camera",
"summary": "Orbit around the target by degrees.",
"call": "window.unroll.camera.orbit(dAzimuth, dPolar?)",
"params": [
{
"name": "dAzimuth",
"type": [
"number"
],
"required": true,
"description": "Degrees around the vertical axis"
},
{
"name": "dPolar",
"type": [
"number"
],
"required": false,
"description": "Degrees up or down"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"camera.zoom": {
"group": "camera",
"summary": "Zoom by a factor (2 is twice as close, 0.5 twice as far).",
"call": "window.unroll.camera.zoom(factor)",
"params": [
{
"name": "factor",
"type": [
"number"
],
"required": true,
"description": "Zoom factor",
"minimum": 0.01,
"maximum": 100
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"camera.pan": {
"group": "camera",
"summary": "Pan the view in millimeters along the screen (x right, y up).",
"call": "window.unroll.camera.pan(dx, dy?)",
"params": [
{
"name": "dx",
"type": [
"number"
],
"required": true,
"description": "mm right"
},
{
"name": "dy",
"type": [
"number"
],
"required": false,
"description": "mm up"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"camera.set": {
"group": "camera",
"summary": "Put the camera at a position looking at a target, both [x, y, z] in scene millimeters.",
"call": "window.unroll.camera.set(position, target?)",
"params": [
{
"name": "position",
"type": [
"array"
],
"required": true,
"description": "[x, y, z]"
},
{
"name": "target",
"type": [
"array"
],
"required": false,
"description": "[x, y, z]"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"camera.reset": {
"group": "camera",
"summary": "Fly back to the default view for the current position (the V key).",
"call": "window.unroll.camera.reset()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.mode": {
"group": "destroy",
"summary": "Enter or leave Destroy mode (damage the scroll as it is, rolled up or unrolled).",
"call": "window.unroll.destroy.mode(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "Destroy mode on"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.tool": {
"group": "destroy",
"summary": "Pick the Destroy tool used by clicks and drags: \"tear\", \"crush\", \"burn\" or \"water\".",
"call": "window.unroll.destroy.tool(name)",
"params": [
{
"name": "name",
"type": [
"string"
],
"required": true,
"description": "Tool",
"enum": [
"tear",
"crush",
"burn",
"water"
]
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.aim": {
"group": "destroy",
"summary": "A screen point that is on the papyrus right now, nearest the center of the view: { x, y, winding }. Use it for the tool calls.",
"call": "window.unroll.destroy.aim()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.tear": {
"group": "destroy",
"summary": "Rip the papyrus with a drag from (x, y). Enters Destroy mode and picks Tear. Returns damage stats before and after.",
"call": "window.unroll.destroy.tear(x, y, options?)",
"params": [
{
"name": "x",
"type": [
"number"
],
"required": true,
"description": "Screen x in CSS pixels from the left of the page",
"minimum": 0
},
{
"name": "y",
"type": [
"number"
],
"required": true,
"description": "Screen y in CSS pixels from the top",
"minimum": 0
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "Optional { dx, dy, steps, ms }: drag vector in CSS px (default 90, -200), number of pointer moves (default 24), time per move in ms (default 16)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.crush": {
"group": "destroy",
"summary": "Press at (x, y) to crumble the papyrus; options.hold (ms, default 700) presses harder and deeper.",
"call": "window.unroll.destroy.crush(x, y, options?)",
"params": [
{
"name": "x",
"type": [
"number"
],
"required": true,
"description": "Screen x in CSS pixels from the left of the page",
"minimum": 0
},
{
"name": "y",
"type": [
"number"
],
"required": true,
"description": "Screen y in CSS pixels from the top",
"minimum": 0
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "Optional { hold }: how long to press, in ms (0 to 5000)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.burn": {
"group": "destroy",
"summary": "Ignite the papyrus at (x, y); embers then creep along the fibers.",
"call": "window.unroll.destroy.burn(x, y, options?)",
"params": [
{
"name": "x",
"type": [
"number"
],
"required": true,
"description": "Screen x in CSS pixels from the left of the page",
"minimum": 0
},
{
"name": "y",
"type": [
"number"
],
"required": true,
"description": "Screen y in CSS pixels from the top",
"minimum": 0
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "Optional { hold }: how long to press, in ms (0 to 5000)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.water": {
"group": "destroy",
"summary": "Pour water at (x, y); options.hold (ms, default 900) widens the wet patch.",
"call": "window.unroll.destroy.water(x, y, options?)",
"params": [
{
"name": "x",
"type": [
"number"
],
"required": true,
"description": "Screen x in CSS pixels from the left of the page",
"minimum": 0
},
{
"name": "y",
"type": [
"number"
],
"required": true,
"description": "Screen y in CSS pixels from the top",
"minimum": 0
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "Optional { hold }: how long to press, in ms (0 to 5000)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.reset": {
"group": "destroy",
"summary": "Undo all damage, clear the debris and put the Sandbox sheets back.",
"call": "window.unroll.destroy.reset()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.sandbox": {
"group": "destroy",
"summary": "Sandbox on or off: the strip laid flat with full fracture physics (Destroy mode only).",
"call": "window.unroll.destroy.sandbox(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "Sandbox on"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.grab": {
"group": "destroy",
"summary": "In the Sandbox, grab the papyrus at (x, y) and pull (crushes instead while the Crush tool is picked). Returns breaks before and after.",
"call": "window.unroll.destroy.grab(x, y, options?)",
"params": [
{
"name": "x",
"type": [
"number"
],
"required": true,
"description": "Screen x in CSS pixels from the left of the page",
"minimum": 0
},
{
"name": "y",
"type": [
"number"
],
"required": true,
"description": "Screen y in CSS pixels from the top",
"minimum": 0
},
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "Optional { dx, dy, steps, ms }: drag vector in CSS px (default 90, -200), number of pointer moves (default 24), time per move in ms (default 16)"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.crumble": {
"group": "destroy",
"summary": "Crumble the whole scroll: it breaks up from the edges into a heap of dust. destroy.reset() restores it.",
"call": "window.unroll.destroy.crumble()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"prizeOnly": {
"group": "scrolls",
"summary": "Show only prize eligible scrolls in the scroll picker, on or off (toggles when omitted). Returns { prizeOnly, visible }.",
"call": "window.unroll.prizeOnly(on)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": true,
"description": "Only prize eligible scrolls"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.shake": {
"group": "destroy",
"summary": "Sandbox only: knock the table, loosening random bits.",
"call": "window.unroll.destroy.shake()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.stats": {
"group": "destroy",
"summary": "Damage numbers (masks, embers, particles, shards, puddles) and Sandbox numbers (nodes, links, breaks, debris).",
"call": "window.unroll.destroy.stats()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"destroy.tools": {
"group": "destroy",
"summary": "The Destroy tools in this build.",
"call": "window.unroll.destroy.tools()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"rollUp": {
"group": "view",
"summary": "Roll the scroll back up with a steady rewind (the Roll up button); false stops it. Toggles if omitted.",
"call": "window.unroll.rollUp(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "Rolling"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"embed.snippet": {
"group": "embed",
"summary": "The iframe snippet for this viewer: { url, html }. options: { state (start at the current view, default true), autoplay, controls (default true), width, height }.",
"call": "window.unroll.embed.snippet(options?)",
"params": [
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ state, autoplay, controls, width, height }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"embed.info": {
"group": "embed",
"summary": "Embed mode: { embedded (?embed=1), framed (inside an iframe), active (the scene has the pointer and wheel), controls, autoplay }.",
"call": "window.unroll.embed.info()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"embed.activate": {
"group": "embed",
"summary": "In embed mode, hand the pointer, touch and wheel to the scene (like clicking the \"Click to interact\" layer), or give them back with false.",
"call": "window.unroll.embed.activate(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "Activate"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"open.fromURL": {
"group": "open",
"summary": "Open your own: fetch a manifest.json or zip from a URL (the server must allow CORS). Nothing is uploaded. On success the page reloads into the new scroll.",
"call": "window.unroll.open.fromURL(url)",
"params": [
{
"name": "url",
"type": [
"string"
],
"required": true,
"description": "http(s) URL"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"open.fromFiles": {
"group": "open",
"summary": "Open your own from file contents: [{ name, data }] where data is an ArrayBuffer, a Uint8Array, a Blob or a base64 string. A zip, or a manifest.json plus its files. On success the page reloads.",
"call": "window.unroll.open.fromFiles(files)",
"params": [
{
"name": "files",
"type": [
"array"
],
"required": true,
"description": "Files"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"open.example": {
"group": "open",
"summary": "Build the synthetic example package. Returns it as base64 (options.download also saves the zip).",
"call": "window.unroll.open.example(options?)",
"params": [
{
"name": "options",
"type": [
"object"
],
"required": false,
"description": "{ download: boolean }"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"open.listLocal": {
"group": "open",
"summary": "Scrolls kept in this browser.",
"call": "window.unroll.open.listLocal()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"open.removeLocal": {
"group": "open",
"summary": "Remove a scroll kept in this browser.",
"call": "window.unroll.open.removeLocal(key)",
"params": [
{
"name": "key",
"type": [
"string"
],
"required": true,
"description": "Local key"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"open.validate": {
"group": "open",
"summary": "Check a manifest object with the page validator, without opening anything.",
"call": "window.unroll.open.validate(manifest)",
"params": [
{
"name": "manifest",
"type": [
"object"
],
"required": true,
"description": "The manifest"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.dragRes": {
"group": "ct",
"summary": "Lower render resolution while the camera moves: \"off\", \"light\" (0.75x) or \"strong\" (0.5x). Without an argument, returns the state.",
"call": "window.unroll.ct.dragRes(mode?)",
"params": [
{
"name": "mode",
"type": [
"string"
],
"required": false,
"description": "off, light or strong",
"enum": [
"off",
"light",
"strong"
]
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.setWindow": {
"group": "ct",
"summary": "X-ray window and level on the 3D scroll, both 0..1 (low = level - window / 2).",
"call": "window.unroll.ct.setWindow(window, level)",
"params": [
{
"name": "window",
"type": [
"number"
],
"required": true,
"description": "Width, 0.005..1"
},
{
"name": "level",
"type": [
"number"
],
"required": true,
"description": "Center, 0..1"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.autoContrast": {
"group": "ct",
"summary": "X-ray auto contrast from the CT atlas percentiles (default from Settings, 1 and 99).",
"call": "window.unroll.ct.autoContrast(pLow?, pHigh?)",
"params": [
{
"name": "pLow",
"type": [
"number"
],
"required": false,
"description": "Low percentile"
},
{
"name": "pHigh",
"type": [
"number"
],
"required": false,
"description": "High percentile"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.fullRange": {
"group": "ct",
"summary": "Show the full X-ray brightness range.",
"call": "window.unroll.ct.fullRange()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.contrast": {
"group": "ct",
"summary": "X-ray window/level state.",
"call": "window.unroll.ct.contrast()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.open": {
"group": "ct",
"summary": "Open the CT volume (MIP in a slab) around a papyrus point: { winding, u, v } or { voxel: [x, y, z] } or { x, y } screen point, with level? (0..5) and size? (64, 128, 192, 256). Streams OME-Zarr chunks from the public S3 bucket.",
"call": "window.unroll.ct.open(where)",
"params": [
{
"name": "where",
"type": [
"object"
],
"required": true,
"description": "Point"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.close": {
"group": "ct",
"summary": "Close the CT volume panel.",
"call": "window.unroll.ct.close()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.state": {
"group": "ct",
"summary": "CT volume state: point, level, size, voxel size, source URL, slab, window, load stats, labels.",
"call": "window.unroll.ct.state()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.setSlab": {
"group": "ct",
"summary": "Slab clip of the CT volume: { on, thickness (voxels), offset (voxels), tiltX, tiltY (degrees), face (point the camera down the normal) }.",
"call": "window.unroll.ct.setSlab(slab)",
"params": [
{
"name": "slab",
"type": [
"object"
],
"required": true,
"description": "Slab fields to change"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.volumeWindow": {
"group": "ct",
"summary": "Window and level of the CT volume MIP, both 0..1.",
"call": "window.unroll.ct.volumeWindow(window, level)",
"params": [
{
"name": "window",
"type": [
"number"
],
"required": true,
"description": "Width"
},
{
"name": "level",
"type": [
"number"
],
"required": true,
"description": "Center"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.volumeAuto": {
"group": "ct",
"summary": "Auto contrast of the CT volume from its own histogram.",
"call": "window.unroll.ct.volumeAuto()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.setDetail": {
"group": "ct",
"summary": "Detail level of the CT volume: 0 is the full 9.4 µm scan, each step halves the resolution. Reloads.",
"call": "window.unroll.ct.setDetail(level)",
"params": [
{
"name": "level",
"type": [
"number"
],
"required": true,
"description": "0..5"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.setSize": {
"group": "ct",
"summary": "Cube size of the CT volume in voxels. Reloads.",
"call": "window.unroll.ct.setSize(size)",
"params": [
{
"name": "size",
"type": [
"number"
],
"required": true,
"description": "64, 128, 192 or 256"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.pick": {
"group": "ct",
"summary": "Turn pick mode on or off: the next click on the papyrus opens the CT volume there.",
"call": "window.unroll.ct.pick(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "On"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.image": {
"group": "ct",
"summary": "The CT volume view as a PNG data URL.",
"call": "window.unroll.ct.image()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.labels": {
"group": "ct",
"summary": "Labeled segments: color every traced segment inside the CT volume cube. Returns the segments found with their colors and voxel counts.",
"call": "window.unroll.ct.labels(on?)",
"params": [
{
"name": "on",
"type": [
"boolean"
],
"required": false,
"description": "On"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.highlight": {
"group": "ct",
"summary": "Highlight one labeled segment by winding id (null clears).",
"call": "window.unroll.ct.highlight(id)",
"params": [
{
"name": "id",
"type": [
"string",
"null"
],
"required": true,
"description": "Winding id, for example w035"
}
],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
},
"ct.source": {
"group": "ct",
"summary": "Where the CT volume of the current scroll streams from (OME-Zarr on s3://vesuvius-challenge-open-data) and the levels it has.",
"call": "window.unroll.ct.source()",
"params": [],
"returns": "Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>"
}
},
"events": {
"change": "Any state change: { changed: [paths], state }",
"unroll": "The unroll position settled: { t }",
"layer": "The layer changed: { layer }",
"focus": "The close-up opened, closed or changed tab: { focus }",
"panel": "A panel opened or closed: { panels }",
"destroy": "Destroy mode, crush or a tool was used: { destroy }",
"camera": "The camera came to rest somewhere new: { camera }",
"settled": "Unroll and camera at rest: { state }",
"scroll": "A scroll is about to open (the page reloads): { id }",
"extension": "An add-on registered a method group, for example bench: { group, methods }",
"error": "A call failed: { method, error }"
},
"urlParams": {
"scroll": "Registry id or local key (alias s)",
"t": "Unroll position 0..1",
"layer": "xray, ink or overlay (alias l; gpu and ct also work)",
"focus": "Winding id: open its close-up",
"view": "Close-up tab: main, overlay or compare",
"compare": "Open the compare swipe at this split position 0..1 (on the focus winding, or the current one)",
"panel": "about, help, open, why or embed",
"embed": "1 for the compact embed layout with click to interact (query string only)",
"autoplay": "1 to play the unroll on load (embed mode)",
"controls": "0 to hide the controls (embed mode)",
"destroy": "1 to start in Destroy mode",
"tool": "Destroy tool: tear, crush, burn or water (implies destroy=1)",
"crush": "1: the same as tool=crush",
"sandbox": "1 to start in the Destroy Sandbox",
"cam": "Camera px,py,pz,tx,ty,tz in scene millimeters",
"shot": "In the query string: a clean frame for share images"
},
"keys": {
"1": "setLayer(\"xray\")",
"2": "setLayer(\"ink\")",
"3": "setLayer(\"overlay\")",
"Space": "play",
"ArrowRight": "setUnroll(t + 0.01)",
"ArrowLeft": "setUnroll(t - 0.01)",
"Shift+Arrow": "setUnroll(t +/- 0.05)",
"Home": "setUnroll(0)",
"End": "setUnroll(1)",
"r": "focus()",
"v": "camera.reset()",
"t": "destroy.mode()",
"x": "destroy.tool(\"tear\")",
"c": "destroy.tool(\"crush\")",
"b": "destroy.tool(\"burn\")",
"w": "destroy.tool(\"water\")",
"u": "rollUp()",
"f": "fullscreen()",
"i": "panel(\"about\")",
"?": "panel(\"help\")",
"Escape": "close panels",
"canvas focused: arrows": "camera.orbit",
"canvas focused: + and -": "camera.zoom",
"canvas focused: Shift+arrows": "camera.pan",
"canvas focused: Enter": "focus()",
"close-up focused: + - arrows 0": "closeup()"
},
"manifestSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Unroll scroll manifest",
"type": "object",
"required": [
"schema",
"id",
"name",
"voxelMM",
"geometry",
"windings",
"layers"
],
"properties": {
"schema": {
"const": "unroll-scroll/1"
},
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"
},
"name": {
"type": "string",
"maxLength": 120
},
"voxelMM": {
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 10
},
"base": {
"type": "string",
"description": "Relative folder that paths start from"
},
"geometry": {
"type": "object",
"required": [
"format"
],
"properties": {
"format": {
"enum": [
"grid-int16",
"mesh"
],
"description": "grid-int16 is for registry scrolls only"
},
"units": {
"enum": [
"voxels",
"mm"
]
},
"axis": {
"enum": [
"z",
"y"
]
},
"index": {
"type": "string",
"description": "Relative path inside the package: no URL, no leading slash, no \"..\""
},
"bin": {
"type": "string",
"description": "Relative path inside the package: no URL, no leading slash, no \"..\""
}
}
},
"windings": {
"description": "A list of { id, mesh } or { range: [first, last], mesh: pattern with {id}, outermost }",
"oneOf": [
{
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"mesh": {
"type": "string",
"description": ".obj, .ply"
}
}
}
},
{
"type": "object",
"required": [
"range"
]
}
]
},
"outermost": {
"enum": [
"first",
"last"
]
},
"layers": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"id",
"role",
"label"
],
"properties": {
"id": {
"type": "string"
},
"role": {
"enum": [
"ct",
"ink",
"overlay"
]
},
"label": {
"type": "string"
},
"shortLabel": {
"type": "string",
"description": "Label where space is tight (layer chip, compare swipe)"
},
"resolutionUM": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Resolution in micrometers"
},
"windings": {
"description": "\"all\", a list of ids, or { range: [first, last] }"
},
"texture": {
"type": "string",
"description": "Per-winding image path pattern containing {id}"
},
"files": {
"type": "object",
"additionalProperties": {
"type": "string",
"description": "Relative path inside the package: no URL, no leading slash, no \"..\""
},
"description": "Winding id to image path, overriding texture"
},
"atlas": {
"type": "string",
"description": "Relative path inside the package: no URL, no leading slash, no \"..\""
},
"atlasFallback": {
"type": "string",
"description": "Relative path inside the package: no URL, no leading slash, no \"..\""
},
"hi": {
"type": "string",
"description": "Close-up image pattern containing {id}"
},
"stats": {
"type": "string",
"description": "Relative path inside the package: no URL, no leading slash, no \"..\""
},
"source": {
"type": "string",
"description": "Where the layer comes from"
},
"aboutHtml": {
"type": "string",
"description": "Registry scrolls only"
}
}
},
"description": "One layer per role; at least one ct or ink layer"
},
"description": {
"type": "string"
},
"credits": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"citation": {
"type": "string"
},
"holder": {
"type": "string"
}
}
},
"dataTerms": {
"type": "object",
"properties": {
"license": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"notes": {
"type": "string"
}
}
},
"display": {
"type": "object",
"description": "Registry scrolls only"
}
},
"x-imageExtensions": [
"png",
"jpg",
"jpeg",
"webp",
"tif",
"tiff"
],
"x-meshExtensions": [
"obj",
"ply"
],
"x-limits": {
"openYourOwn": {
"maxFileBytes": 67108864,
"maxTotalBytes": 268435456,
"maxZipBytes": 134217728,
"maxWindings": 64,
"maxVertices": 2000000,
"maxTextureSide": 8192
},
"registry": {
"maxFileBytes": 209715200,
"maxTotalBytes": 2000000000,
"maxZipBytes": 134217728,
"maxWindings": 400,
"maxVertices": 2000000,
"maxTextureSide": 8192
}
}
}
}