unroll / polish.js
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
4.34 kB
// keyboard shortcuts and the help overlay (press ?). Drives the page through its own buttons,
// plus the small camera hooks app.js exposes on window.unrollView.
const $ = (id) => document.getElementById(id);
const KEYS = [
['Space', 'Play or pause the unroll'],
['← →', 'Step the unroll (hold Shift for bigger steps)'],
['Home / End', 'Jump to rolled / fully unrolled'],
['1 2 3', 'X-ray, published ink, ink_9um'],
['R', 'Read the current winding close up'],
['V', 'Reset the camera view'],
['U', 'Roll it back up (again to stop)'],
['T', 'Destroy on or off'],
['X C B W', 'Destroy tools: Tear, Crush, Burn, Water'],
['K', 'Crumble the whole scroll to dust (Reset restores it)'],
['F', 'Full screen'],
['I', 'About this page'],
['?', 'Show or hide this help'],
['Esc', 'Close any panel'],
];
const help = document.createElement('section');
help.id = 'help';
help.className = 'sheet help';
help.hidden = true;
help.setAttribute('role', 'dialog');
help.setAttribute('aria-modal', 'false');
help.setAttribute('aria-labelledby', 'helpTitle');
help.innerHTML = `<div class="sheet-head"><div><div class="mono dim">KEYBOARD</div><h2 id="helpTitle">Shortcuts</h2></div>
<button class="icon" id="hClose" aria-label="Close" data-testid="help-close" data-api="panel('help', false)">✕</button></div>
<dl class="keys mono">${KEYS.map(([k, d]) => `<dt>${k.split(' ').map((x) => (x === '/' ? ' / ' : `<kbd>${x}</kbd>`)).join(' ')}</dt><dd>${d}</dd>`).join('')}</dl>
<p class="mono dim small help-foot">Drag to orbit, scroll or pinch to zoom, click a sheet to read it.</p>`;
document.body.appendChild(help);
const tools = document.querySelector('.tools');
if (tools && !$('btnHelp')) {
const b = document.createElement('button');
b.className = 'icon';
b.id = 'btnHelp';
b.title = 'Keyboard shortcuts (?)';
b.setAttribute('aria-label', 'Keyboard shortcuts');
b.textContent = '?';
b.dataset.testid = 'help'; b.dataset.api = "panel('help')";
tools.prepend(b);
b.onclick = () => toggleHelp();
}
let opener = null;
function toggleHelp(force) {
const show = force ?? help.hidden;
if (show) { opener = document.activeElement; help.hidden = false; $('hClose').focus(); }
else if (!help.hidden) { help.hidden = true; opener?.focus?.(); }
}
$('hClose').onclick = () => toggleHelp(false);
const click = (id) => { const el = $(id); if (el && !el.hidden) { el.click(); return true; } return false; };
const sheetOpen = () => ['focus', 'about', 'openSheet'].some((id) => $(id) && !$(id).hidden);
addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.target.closest?.('textarea,select,input:not([type=range])')) return;
const k = e.key;
if (k === '?' || (k === '/' && e.shiftKey)) { e.preventDefault(); toggleHelp(); return; }
if (k === 'Escape') { toggleHelp(false); return; }
if (!help.hidden || sheetOpen()) return;
const range = $('scrubIn');
const step = (d) => { range.value = Math.max(0, Math.min(1000, +range.value + d)); range.dispatchEvent(new Event('input', { bubbles: true })); range.dispatchEvent(new Event('change', { bubbles: true })); };
switch (k.toLowerCase()) {
case '1': document.querySelector('[data-layer=xray]')?.click(); break;
case '2': document.querySelector('[data-layer=ink]')?.click(); break;
case '3': document.querySelector('[data-layer=gpu]:not([hidden])')?.click(); break;
case 'r': click('btnRead'); break;
case 'v': window.unrollView?.reset(); break;
case 't': click('btnTear'); break;
case 'c': click('btnCrush'); break;
case 'x': click('toolTear'); break;
case 'b': click('toolBurn'); break;
case 'w': click('toolWater'); break;
case 'k': window.unrollView?.crumble(); break;
case 'u': click('btnRollUp'); break;
case 'i': click('btnInfo'); break;
case 'f':
if (document.fullscreenElement) document.exitFullscreen?.();
else document.documentElement.requestFullscreen?.().catch(() => {});
break;
case 'home': e.preventDefault(); step(-1000); break;
case 'end': e.preventDefault(); step(1000); break;
case 'arrowright': case 'arrowleft':
// app.js steps by 1 percent; Shift adds a 5 percent jump
if (e.shiftKey && e.target === document.body) step(k === 'ArrowRight' ? 40 : -40);
break;
default: return;
}
});