// Core interface-scale capability — the app-wide "Interface size" preference. // // Owns a single user setting: a multiplier applied to the ROOT font-size, so // the rem-based v3 chrome (menus, buttons, text, spacing) scales together. This // is the DOM lever — it deliberately does NOT touch the gameplay highway canvas // (which is sized in device pixels, not rem), so scaling the UI never changes // playback resolution or FPS. // // It is exposed as a host read/write API on `window.feedBack.scale` so surfaces // that can't inherit `rem` — canvas / WebGL renderers such as the note-highway // HUD or results scorecards — can read the number via `feedBack.scale.get()` // and follow `scale:changed`. Shape mirrors the other host capabilities (a // frozen, versioned object) and the working-tuning read-API: synchronous // `get()`, a `set()` mutator, and a change event that also fires once on load. // // The visual apply ALSO runs pre-paint from a tiny inline script (see // index.html) so there is no flash-of-reflow on load; this module is the // authoritative owner and re-applies idempotently. (function () { 'use strict'; window.feedBack = window.feedBack || {}; if (window.feedBack.scale && window.feedBack.scale.version === 1) return; var STORE_KEY = 'v3-interface-scale'; var MIN = 0.85, MAX = 1.50, DEFAULT = 1.0; // The named presets rendered by the Settings segmented control. Kept here so // the control and any consumer read the ladder from one source of truth. var PRESETS = [ { step: 'small', value: 0.90 }, { step: 'medium', value: 1.00 }, { step: 'large', value: 1.15 }, { step: 'x-large', value: 1.30 }, ]; function clamp(n) { n = Number(n); if (!isFinite(n)) return DEFAULT; return Math.min(MAX, Math.max(MIN, n)); } function stepFor(value) { for (var i = 0; i < PRESETS.length; i++) { if (Math.abs(PRESETS[i].value - value) < 0.001) return PRESETS[i].step; } return 'custom'; } function read() { try { var raw = localStorage.getItem(STORE_KEY); if (raw == null) return DEFAULT; return clamp(parseFloat(raw)); } catch (_) { return DEFAULT; } } var current = read(); // Apply to the DOM. The lever is a RELATIVE root font-size (a percentage of // the user-agent base), never a px literal — so a user who raised their // browser/OS base font size is respected, not silently overridden. We also // publish the always-present `--fb-scale` token for canvas consumers and CSS. function apply(value) { var el = document.documentElement; if (!el) return; el.style.setProperty('--fb-scale', String(value)); // Medium (1.0) clears the inline override so default rendering is // byte-identical to before this feature existed (zero blast radius). el.style.fontSize = (Math.abs(value - 1) < 0.001) ? '' : (value * 100).toFixed(2) + '%'; } function persist(value) { try { if (Math.abs(value - DEFAULT) < 0.001) localStorage.removeItem(STORE_KEY); else localStorage.setItem(STORE_KEY, String(value)); } catch (_) { /* private mode */ } } function announce() { try { if (typeof window.feedBack.emit === 'function') { window.feedBack.emit('scale:changed', { value: current, step: stepFor(current) }); } } catch (_) { /* noop */ } } // Hydrate on load (idempotent with the pre-paint inline script). apply(current); window.feedBack.scale = Object.freeze({ version: 1, min: MIN, max: MAX, default: DEFAULT, // Synchronous — valid immediately after this module parses. get: function () { return { value: current, step: stepFor(current) }; }, // A copy of the preset ladder, so a UI can render it from one source. presets: function () { return PRESETS.map(function (p) { return { step: p.step, value: p.value }; }); }, // Set + apply + persist + announce. Pass { persist:false } for a // transient preview (e.g. a live slider drag) that shouldn't be written. set: function (value, opts) { var v = clamp(value); current = v; apply(v); if (!opts || opts.persist !== false) persist(v); announce(); return current; }, }); // Announce once after the document parses, so any listener wired during page // load can sync without special-casing (consumers may also just call get()). // `defer` runs this at readyState 'interactive' — later scripts have not // evaluated yet, so wait for DOMContentLoaded (see static/v3/index.html). if (document.readyState !== 'complete') { document.addEventListener('DOMContentLoaded', announce, { once: true }); } else { announce(); } })();