{ "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 } } } }