// ==UserScript== // @name Grid Mode — Turn Any Website Into a Cyberpunk Terminal (Alt+G) // @namespace https://schema.cx/grid-mode // @version 1.7.3 // @description Press Alt+G on any website to see it rendered from inside the Grid: a cyberpunk terminal with a scan-line boot, target-lock brackets, decoding headlines and duotone images. Calm mode is a clean dark mode for all-day reading. Five colour lenses. Alt+G again and the page is untouched. No tracking. // @author Schema // @homepageURL https://schema.cx/?ref=grid-mode // @supportURL https://github.com/sch8ma/grid-mode/issues // @icon https://schema.cx/favicon-32x32.png // @license MIT // @tag dark mode // @tag theme // @tag cyberpunk // @tag terminal // @match *://*/* // @exclude *://schema.cx/* // @exclude *://*.schema.cx/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect api.schema.cx // ==/UserScript== /* * GRID MODE — the web, rendered from inside the Grid. * * Alt+G enter / leave the Grid (⌥+G on a Mac) * Alt+Shift+G next lens: GRID · SIGNAL · PULSE · SOUL · FIELD * the HUD mark opens the panel: Calm / Cinematic, lenses, per-site memory * * How it works: * READ Every element is read ONCE, the moment it is unmarked — at entry, or * when the page inserts it (a MutationObserver runs before the next * paint). Its own colours decide what it becomes: an opaque surface turns * into a panel or a chip, coloured text keeps its meaning on Schema's * palette, numbers become monospaced data. The result is one attribute, * `data-sgm`, holding a few tokens. * PAINT One stylesheet repaints only marked elements. Because an unmarked * element is never painted, its original style can always be read * directly: the sheet is never switched off, so the page is never * restyled twice to look at it. * LIVE A Shadow-DOM layer, isolated from the page's CSS, draws the world grid, * scanlines, target lock, click pings, the HUD and the panel. * * Leave the Grid and the page is exactly what it was. * * For the ones who read source: the Grid rewards curiosity. Ensina answers to * her name. Some lenses are earned, and old games remember how to open doors. * — the Architects * * Privacy: no analytics, no identifiers. While the Grid is on, the HUD asks * api.schema.cx for ONE public, cookieless number (how many Zones are live), at * most once every 30 minutes. Settings stay in your userscript manager. */ (function () { 'use strict'; const isHotkey = (e) => e.altKey && !e.ctrlKey && !e.metaKey && e.code === 'KeyG' && !e.repeat; // Inside a frame (a consent dialog, an embedded player) the script does ONE // thing: hand Alt+G up to the page, so the hotkey works wherever focus sits. if (window.top !== window.self) { window.addEventListener( 'keydown', (e) => { if (!isHotkey(e)) return; e.preventDefault(); try { window.top.postMessage({ schemaGridMode: e.shiftKey ? 'lens' : 'toggle' }, '*'); } catch (err) { /* a sandboxed frame cannot reach the top — nothing to do */ } }, true, ); return; } if (window.__schemaGridMode) return; window.__schemaGridMode = true; // At document-start the element may not exist yet — wait for it. if (document.documentElement) main(document.documentElement); else { const wait = new MutationObserver(() => { if (!document.documentElement) return; wait.disconnect(); main(document.documentElement); }); wait.observe(document, { childList: true }); } function main(html) { if (!(html instanceof HTMLElement)) return; // XML / SVG documents // ─── Constants ────────────────────────────────────────────────────────── const VERSION = '1.7.3'; const HOST = location.hostname || 'local'; const JOIN_URL = 'https://schema.cx/?ref=grid-mode'; const STATS_URL = 'https://api.schema.cx/api/stats'; const FONT_CSS = 'https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700;800&family=Fira+Code:wght@400;500;600&display=swap'; const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent); const ALT = IS_MAC ? '⌥' : 'ALT'; const REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const Z = 2147483600; // the top of the stacking world, minus room for our own layers const ATTR = 'data-sgm'; // Schema's accent families: `acc` is the default step, `acc2` the light ink // step, `hue` rotates the duotone image tint onto the lens. const LENSES = { grid: { name: 'GRID', acc: '#D63A32', acc2: '#F08A84', hue: '-32deg' }, signal: { name: 'SIGNAL', acc: '#5AA7FF', acc2: '#A6CEFF', hue: '178deg' }, pulse: { name: 'PULSE', acc: '#D7B84F', acc2: '#ECD79A', hue: '12deg' }, soul: { name: 'SOUL', acc: '#D2A6FF', acc2: '#ECCBFF', hue: '236deg' }, field: { name: 'FIELD', acc: '#63C174', acc2: '#A7E0B0', hue: '96deg' }, // Earned, not given. The Architects see the Grid in black and white. void: { name: 'VOID', acc: '#EDF0F5', acc2: '#FFFFFF', hue: '0deg', sat: 0, secret: true }, }; const LENS_KEYS = Object.keys(LENSES); // A page's coloured text and surfaces keep their meaning, mapped onto the palette. const HUE_INK = { red: '#F08A84', amber: '#F0C885', gold: '#ECD79A', green: '#A7E0B0', blue: '#A6CEFF', violet: '#E3B8FF', pink: '#FE7AB6', }; const HUE_FILL = { red: '#D63A32', amber: '#D99A35', gold: '#D7B84F', green: '#63C174', blue: '#5AA7FF', violet: '#D2A6FF', pink: '#FC538D', }; // Two ways to live in the Grid. Calm is a dark mode you keep on all day; // Cinematic is the full render. Fine-tuning a layer makes the mode "custom". const LAYER_KEYS = ['type', 'scan', 'backdrop', 'duo', 'decode', 'lock']; const PRESETS = { calm: { type: false, scan: false, backdrop: true, duo: false, decode: false, lock: false }, cinematic: { type: true, scan: true, backdrop: true, duo: true, decode: true, lock: true }, }; // Canvas-driven apps a repaint can only hurt. "Every site" leaves them alone; // Alt+G still works there on request. const APP_HOSTS = /(^|\.)(docs\.google\.com|drive\.google\.com|meet\.google\.com|earth\.google\.com|figma\.com|canva\.com|miro\.com|excalidraw\.com|tldraw\.com|photopea\.com|diagrams\.net|draw\.io|onshape\.com|zoom\.us|teams\.microsoft\.com|teams\.live\.com)$/; const isAppPage = () => APP_HOSTS.test(HOST) || (/(^|\.)google\.[a-z.]+$/.test(HOST) && location.pathname.startsWith('/maps')); const MARK_PATHS = [ "M3455 6234 c-115 -19 -285 -58 -342 -80 -38 -14 -143 -45 -235 -69 -200 -53 -304 -91 -553 -203 -104 -46 -259 -116 -344 -154 -156 -70 -284 -146 -377 -224 -61 -52 -91 -64 -121 -53 -53 20 -13 102 126 259 60 68 62 72 25 45 -23 -17 -107 -91 -188 -165 -80 -74 -169 -147 -197 -161 -56 -28 -81 -60 -105 -134 -21 -65 -38 -83 -63 -68 -17 10 -19 8 -24 -30 -9 -65 9 -215 32 -281 12 -33 21 -69 21 -80 0 -12 7 -30 15 -40 19 -26 19 -60 -1 -102 -21 -43 -47 -44 -132 -4 -34 16 -62 27 -62 25 0 -3 43 -51 95 -107 52 -57 95 -106 95 -110 0 -5 -9 -8 -20 -8 -27 0 -231 44 -349 75 -52 14 -95 24 -97 22 -2 -2 74 -47 169 -100 94 -53 186 -106 202 -118 l30 -21 -75 6 -75 6 61 -39 c34 -22 71 -44 83 -50 31 -17 26 -27 -24 -46 -34 -13 -64 -16 -122 -13 l-78 5 64 -29 c36 -17 117 -43 180 -60 120 -31 179 -56 292 -126 208 -127 384 -200 453 -187 36 7 179 -36 269 -82 55 -28 127 -89 127 -109 0 -7 -15 -21 -32 -30 -18 -9 -73 -56 -123 -103 -111 -107 -147 -132 -302 -210 -89 -44 -126 -68 -137 -88 -23 -44 -99 -113 -266 -238 -85 -65 -146 -115 -135 -111 37 11 127 26 156 26 22 0 29 -5 29 -20 0 -22 -21 -36 -90 -56 -68 -20 -272 -126 -329 -171 -27 -21 -53 -50 -60 -64 -13 -30 -56 -56 -166 -98 -44 -17 -89 -38 -100 -47 -19 -14 -17 -14 23 -8 69 11 53 -15 -80 -130 -108 -92 -248 -234 -248 -250 0 -11 103 45 179 96 141 96 264 137 441 148 88 5 142 14 183 29 32 12 94 24 138 28 87 7 92 2 67 -52 -6 -14 -9 -27 -6 -30 3 -3 99 33 214 81 115 47 272 108 349 136 77 28 187 72 244 99 88 41 113 49 163 49 39 0 90 10 146 29 48 16 112 37 142 46 30 9 93 39 139 67 47 28 119 66 160 85 42 18 142 67 223 108 80 41 155 75 167 75 32 0 266 -147 377 -237 95 -77 184 -138 266 -183 21 -11 42 -29 48 -39 15 -29 12 -83 -5 -108 -14 -20 -13 -23 15 -40 16 -9 30 -25 30 -34 0 -29 -52 -81 -105 -105 l-50 -23 130 -1 c273 -3 272 -44 -8 -258 -78 -59 -164 -125 -191 -146 -69 -53 -126 -77 -208 -87 -38 -5 -88 -18 -111 -29 -72 -36 -190 -107 -242 -146 -58 -44 -107 -59 -248 -74 -56 -6 -172 -27 -257 -46 -176 -39 -336 -63 -629 -95 -219 -24 -239 -23 -338 21 -30 13 -43 25 -43 38 0 22 125 149 168 171 15 7 54 37 87 66 33 28 87 70 120 92 75 51 190 143 179 143 -5 0 -25 -4 -44 -9 -111 -28 -117 -21 -32 41 119 87 260 215 285 257 l18 31 -34 -16 c-73 -35 -92 18 -29 77 69 65 176 145 352 263 96 65 182 125 190 133 32 33 -513 -253 -559 -293 -50 -43 -134 -94 -158 -94 -8 0 -45 -14 -83 -30 -38 -17 -81 -31 -97 -31 -21 -1 -23 -3 -8 -6 45 -11 33 -22 -47 -47 -91 -28 -177 -78 -380 -221 -110 -78 -123 -85 -126 -65 -2 12 -9 19 -15 17 -20 -6 -85 -98 -127 -180 -37 -73 -40 -84 -40 -161 0 -165 36 -270 134 -387 133 -159 244 -220 551 -302 112 -29 131 -31 280 -31 202 1 541 34 808 80 248 42 662 192 1050 380 163 79 302 144 307 144 6 0 15 -11 21 -24 16 -34 -23 -102 -119 -206 l-77 -84 86 64 c48 36 115 89 150 119 l64 54 3 -29 c6 -58 13 -57 41 2 31 66 53 88 93 98 37 8 73 56 114 150 19 44 36 82 40 85 8 9 24 -21 24 -49 l1 -25 11 25 c6 14 10 86 10 160 0 93 6 166 19 235 10 55 19 110 19 122 0 22 9 27 73 39 l37 6 -45 47 c-61 62 -115 144 -122 185 l-5 33 103 8 104 7 -94 44 -93 44 3 87 4 88 -21 -24 c-12 -14 -26 -21 -33 -17 -16 10 -59 97 -91 186 l-28 74 -113 38 c-148 49 -366 157 -418 206 -45 43 -60 84 -40 109 8 9 32 30 54 46 32 24 35 28 15 23 -14 -3 -74 -9 -135 -12 -127 -7 -168 3 -163 38 4 27 94 102 207 171 69 43 95 67 149 137 37 47 85 108 108 135 23 28 56 73 73 100 37 59 120 146 176 185 56 39 407 400 423 435 8 17 19 41 26 55 7 15 42 42 86 67 155 86 179 113 99 113 -50 0 -51 19 -1 48 40 22 165 147 278 276 81 93 64 87 -94 -32 -53 -40 -119 -86 -147 -102 -76 -43 -189 -77 -300 -90 -75 -8 -114 -18 -156 -40 -56 -28 -246 -76 -258 -65 -3 4 4 25 17 47 12 23 21 42 20 43 -4 4 -327 -149 -621 -294 -257 -126 -271 -131 -315 -125 -43 5 -58 0 -187 -62 -77 -37 -194 -104 -260 -148 -66 -44 -148 -90 -185 -103 -36 -13 -86 -33 -113 -45 l-47 -21 -25 23 c-13 13 -29 38 -35 56 l-11 34 -31 -16 c-52 -27 -104 -12 -215 63 -54 37 -192 118 -308 181 -115 63 -248 138 -295 166 -83 50 -84 52 -103 112 -32 99 -25 114 106 236 61 58 130 116 153 130 66 42 244 116 504 209 l245 88 -105 1 c-146 2 -212 27 -149 57 84 40 444 205 546 249 204 89 273 102 543 105 l175 1 90 -43 c50 -24 93 -45 96 -46 4 -2 4 8 2 22 -4 17 -1 25 8 25 24 -1 93 -36 114 -59 19 -22 19 -23 0 -57 -22 -41 -70 -81 -135 -114 -46 -23 -482 -331 -477 -336 2 -2 52 15 111 37 59 23 110 39 113 35 22 -22 -67 -117 -289 -309 -36 -32 -73 -69 -82 -82 l-16 -24 35 15 c46 19 91 18 99 -2 11 -28 -25 -76 -143 -197 l-115 -118 74 27 c52 18 127 61 240 138 125 85 183 117 244 138 55 18 75 29 63 34 -32 12 -16 29 61 66 72 34 253 169 347 258 22 21 51 43 64 50 24 12 24 12 18 -24 -3 -20 -4 -36 -2 -36 10 0 102 151 126 208 53 128 46 260 -24 407 -78 164 -281 318 -488 369 -180 44 -534 84 -624 70z", "M3592 2548 c-29 -29 3 -88 48 -88 59 0 56 69 -3 90 -36 12 -31 12 -45 -2z" ]; // ─── Storage ──────────────────────────────────────────────────────────── const store = { get(key, fallback) { try { if (typeof GM_getValue === 'function') return GM_getValue(key, fallback); } catch (e) { /* fall through to localStorage */ } try { const raw = localStorage.getItem('sgm:' + key); return raw == null ? fallback : JSON.parse(raw); } catch (e) { return fallback; } }, set(key, value) { try { if (typeof GM_setValue === 'function') return void GM_setValue(key, value); } catch (e) { /* fall through */ } try { localStorage.setItem('sgm:' + key, JSON.stringify(value)); } catch (e) { /* private mode — settings simply do not persist */ } }, }; const stored = store.get('prefs', {}) || {}; const prefs = { lens: LENSES[stored.lens] ? stored.lens : 'grid', layers: Object.assign({}, PRESETS.cinematic, stored.layers), sound: !!stored.sound, everywhere: !!stored.everywhere, void: !!stored.void, hud: stored.hud === 'mini' || stored.hud === 'full' ? stored.hud : null, }; if (LENSES[prefs.lens].secret && !prefs.void) prefs.lens = 'grid'; const lensList = () => LENS_KEYS.filter((k) => !LENSES[k].secret || prefs.void); const sites = store.get('sites', {}) || {}; const savePrefs = () => store.set('prefs', prefs); const saveSites = () => store.set('sites', sites); const siteOn = () => sites[HOST] === true || (prefs.everywhere && sites[HOST] !== false && !isAppPage()); // Turning the Grid on is a choice for this site in this tab, not for one page: // it follows you from page to page until you leave it. Per-tab storage, so it // never leaks into other tabs or outlives the session. const session = { get: () => { try { return sessionStorage.getItem('sgm:on') === '1'; } catch (e) { return false; } }, set: (on) => { try { if (on) sessionStorage.setItem('sgm:on', '1'); else sessionStorage.removeItem('sgm:on'); } catch (e) { /* storage refused: the Grid simply ends with the page */ } }, }; const modeOf = () => Object.keys(PRESETS).find((m) => LAYER_KEYS.every((k) => prefs.layers[k] === PRESETS[m][k])) || 'custom'; const cinematic = () => modeOf() !== 'calm'; const hudMini = () => (prefs.hud ? prefs.hud === 'mini' : !cinematic()); // ─── Small helpers ────────────────────────────────────────────────────── const SVGNS = 'http://www.w3.org/2000/svg'; const fmt = (n) => Number(n).toLocaleString('en-US'); const onReady = (fn) => { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }; // DOM builder — no innerHTML anywhere, so pages that enforce Trusted Types work. function h(tag, props, ...kids) { const el = document.createElement(tag); if (props) { for (const [k, v] of Object.entries(props)) { if (v == null || v === false) continue; if (k === 'class') el.className = v; else if (k === 'text') el.textContent = v; else if (k === 'style') for (const [p, val] of Object.entries(v)) el.style.setProperty(p, val); else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v); else el.setAttribute(k, v === true ? '' : String(v)); } } for (const kid of kids.flat()) if (kid != null && kid !== false) el.append(kid); return el; } // The Schema mark, always in brand red whatever the lens. function mark(cls) { const svg = document.createElementNS(SVGNS, 'svg'); svg.setAttribute('viewBox', '0 0 6260 7070'); svg.setAttribute('aria-hidden', 'true'); if (cls) svg.setAttribute('class', cls); const g = document.createElementNS(SVGNS, 'g'); g.setAttribute('fill', '#D63A32'); for (const d of MARK_PATHS) { const p = document.createElementNS(SVGNS, 'path'); p.setAttribute('d', d); g.appendChild(p); } svg.appendChild(g); return svg; } // A stylesheet that survives strict page CSP: a constructable sheet first (not // subject to inline-style rules), a