var BBS = {}; /* One ANSI ramp per session, and the dark/paper switch. * * Boards rotated their welcome screens; this draws a palette instead. It is * held in sessionStorage so it stays put while you read and changes when you * come back, rather than flickering on every page load. * * Every dark ramp is three of the sixteen EGA indices, ordered dark to bright, * and none of them bottom out on the frame blue, which would dissolve the * logo's feet into the chrome. Without JS the CSS defaults apply and nothing * is lost. * * On paper the ordering reverses: presence is ink rather than light, so the * feet are the palest step and the crown the darkest, which keeps the weight * where the dark version puts it. None of them start at the frame tone, for * the same reason as above. * * This runs from , before first paint, so neither the ramp nor the * theme flashes. Dark is the default and the no-JS result; paper is opt in * and remembered in localStorage rather than per session, because it is a * preference and not a welcome screen. */ (function () { var RAMPS = [ ['#0000aa', '#00aaaa', '#55ffff'], // ice ['#0000aa', '#00aaaa', '#ffffff'], // ice/white ['#00aaaa', '#55ffff', '#ffffff'], // shallow ['#555555', '#00aaaa', '#55ffff'], // slate cyan ['#00aa00', '#55ff55', '#ffffff'], // moss ['#555555', '#00aa00', '#55ff55'], // slate green ['#00aa00', '#00aaaa', '#55ffff'], // verdigris ['#aa0000', '#aa5500', '#ffff55'], // ember ['#aa0000', '#ff5555', '#ffff55'], // fire ['#aa5500', '#ff5555', '#ffff55'], // rust ['#aa5500', '#ffff55', '#ffffff'], // amber ['#555555', '#aaaaaa', '#ffffff'], // mono ['#555555', '#aaaaaa', '#55ffff'], // cold mono ['#aa00aa', '#ff55ff', '#ffffff'], // violet ['#aa0000', '#aa00aa', '#ff55ff'] // sunset ]; /* The paper ramps, pale to dark, off the same warm grey scale as bbs.css. */ var PAPER = [ ['#887a64', '#655c50', '#38342e'], // paper ['#887a64', '#534d43', '#1e1c19'], // ink ['#756b5b', '#534d43', '#2b2824'], // umber ['#756b5b', '#47423a', '#1e1c19'], // strong ['#655c50', '#47423a', '#2b2824'], // close ['#887a64', '#756b5b', '#534d43'], // soft ['#534d43', '#38342e', '#1e1c19'], // heavy ['#38342e', '#655c50', '#887a64'], // sunlit, the one that runs the other way ['#a0917c', '#756b5b', '#2b2824'] // wide ]; var i; try { i = sessionStorage.getItem('ramp'); if (i === null) { i = Math.floor(Math.random() * RAMPS.length); sessionStorage.setItem('ramp', i); } } catch (e) { i = Math.floor(Math.random() * RAMPS.length); } BBS.ramp = function (theme) { var set = theme === 'light' ? PAPER : RAMPS; var r = set[i % set.length]; var s = document.documentElement.style; s.setProperty('--l1', r[0]); s.setProperty('--l2', r[1]); s.setProperty('--l3', r[2]); }; var t; try { t = localStorage.getItem('theme'); } catch (e) { /* private mode */ } if (t === 'light') document.documentElement.setAttribute('data-theme', 'light'); BBS.ramp(t === 'light' ? 'light' : 'dark'); })(); /* The prompt accepts commands. * * Numbers mean different things depending on the screen, which is how a board * worked: at the main menu they select an area, inside an area they select a * message. Nothing has to disambiguate because no two things on one screen * claim the same key. Anything typeable carries data-key and the chrome that * marks it, so the brackets are a promise rather than decoration. */ (function () { // The script sits in so the ramp applies before first paint; the // command handler has to wait for a DOM to read its keys out of. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { var buf = '', timer; var out = document.getElementById('typed'); if (!out) return; var hint = document.createElement('span'); hint.className = 'hint'; out.parentNode.insertBefore(hint, out.nextSibling.nextSibling); function keys() { var m = {}; [].forEach.call(document.querySelectorAll('[data-key]'), function (a) { m[String(a.dataset.key).trim().toUpperCase()] = { href: a.getAttribute('href'), cmd: a.dataset.cmd, label: a.textContent.trim().replace(/\s+/g, ' ').slice(0, 44) }; }); return m; } var MAP = keys(); // Echo what the number is pointing at, so a pause is informative rather than // just a delay: you can see it is right and wait, keep typing, or hit Enter. function show() { out.textContent = buf; var hit = MAP[buf]; hint.textContent = hit ? ' ' + hit.label : ''; } function go(k) { var hit = MAP[k]; if (!hit) return false; if (hit.cmd === 'theme') { flip(); return true; } location.href = hit.href; return true; } /* Dark or paper, as a command rather than a button: a board had no widgets, it had a letter you typed. The entry names where you are going, so it is [L]ight on a dark screen and [D]ark on a light one, and the key moves with the label. Injected rather than written into the template, so a reader without JS is not offered a switch that cannot work. */ var root = document.documentElement; var slot = document.getElementById('themecmd'); function theme() { return root.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; } function draw() { if (!slot) return; var next = theme() === 'light' ? 'dark' : 'light'; var k = next.charAt(0).toUpperCase(); slot.innerHTML = '[' + k + ']' + next.slice(1) + ''; slot.firstChild.addEventListener('click', function (e) { e.preventDefault(); flip(); }); MAP = keys(); } function flip() { var next = theme() === 'light' ? 'dark' : 'light'; if (next === 'light') root.setAttribute('data-theme', 'light'); else root.removeAttribute('data-theme'); try { localStorage.setItem('theme', next); } catch (e) { /* private mode */ } BBS.ramp(next); draw(); } draw(); addEventListener('keydown', function (e) { if (e.metaKey || e.ctrlKey || e.altKey) return; var k = e.key; clearTimeout(timer); if (k === 'Enter') { if (buf && !go(buf)) { out.textContent = ''; } buf = ''; show(); e.preventDefault(); return; } if (k === 'Backspace') { buf = buf.slice(0, -1); show(); e.preventDefault(); return; } if (k === 'Escape') { buf = ''; show(); e.preventDefault(); return; } if (/^[0-9]$/.test(k)) { buf += k; show(); clearTimeout(timer); var longer = Object.keys(MAP).some(function (x) { return x.length > buf.length && x.indexOf(buf) === 0; }); if (!longer) { // Nothing longer can match, so settle it now: go, or drop what was // typed so a mistyped number never sticks at the prompt. if (MAP[buf]) { go(buf); } else { buf = ''; show(); } } else if (MAP[buf]) { // Ambiguous: "1" is an area and "12" is an entry further down the same // screen. Terminal menus settled this on a short pause rather than // making every selection need Enter, so this does too. // 1200ms, because the message index runs past 40 and reaching for a // second digit takes longer than a confirmation would. Enter commits // at once for anyone who does not want to wait. timer = setTimeout(function () { go(buf); }, 1200); } e.preventDefault(); return; } if (/^[a-zA-Z]$/.test(k)) { if (go(k.toUpperCase())) e.preventDefault(); } }); } })(); /* The address is stored rot13 so it is not sitting in the HTML for a scraper to * lift. Same trick as tobias.raayoni-last.com. Without JS the fallback text * still reads, spelled out, so nothing is lost but the click. */ (function () { function rot13(s) { return s.replace(/[a-zA-Z]/g, function (c) { return String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26); }); } function init() { [].forEach.call(document.querySelectorAll('.mail[data-r]'), function (el) { var a = document.createElement('a'); a.href = 'mailto:' + rot13(el.dataset.r); a.textContent = rot13(el.dataset.r); el.replaceWith(a); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();