/* Subliminal image flasher. * * Reads /subliminals.json (a live listing of the drop folder served at * /audio/subliminals/), preloads the images, and briefly flashes a random one over the * whole viewport at randomized intervals. Used during the antechamber wait AND while the * main session audio plays -- so the subject is fed images they never consciously read. * * The flash is deliberately faint and brief (true subliminal): ~90ms at low opacity, * every 4-8s. The layer is position:fixed with pointer-events:none, so it floats over the * page without ever intercepting clicks. It sits below the trance overlay (z-50). * * start()/stop() are idempotent. If the folder is empty or the manifest fails to load it * stays silent and invisible -- a no-op, like the antechamber bed when its bank is empty. * * Mirrors the persistence trick in loop.js: state lives on `window`, the layer is created * once on , so the 1.5s HTMX progress-poll swaps of #audio-area never disturb it. */ (function () { "use strict"; var MANIFEST = "/subliminals.json"; var FLASH_MS = 90; // how long a single image is visible (true subliminal) var GAP_MIN = 4000; // min ms between flashes var GAP_MAX = 8000; // max ms between flashes (randomized for an organic cadence) var FIRST_MAX = 2500; // cap on the very first delay so something shows soon var OPACITY = 0.22; // peak opacity -- faint, hard to consciously catch var imgs = []; // image urls var queue = []; // upcoming urls (reshuffled when drained) var state = "idle"; // idle | loading | running | stopped var timer = null; // pending "flash next" timer var layer = null; // the fixed full-viewport overlay var hideTimer = null; // pending "hide current flash" timer function shuffle(a) { a = a.slice(); for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } function nextUrl() { if (!queue.length) queue = shuffle(imgs); return queue.shift(); } function ensureLayer() { if (layer) return; layer = document.createElement("div"); layer.id = "subliminal-layer"; layer.setAttribute("aria-hidden", "true"); layer.style.cssText = "position:fixed;inset:0;z-index:40;pointer-events:none;display:flex;" + "align-items:center;justify-content:center;opacity:0;" + "transition:opacity 60ms ease-out;"; var img = document.createElement("img"); img.id = "subliminal-img"; img.alt = ""; img.style.cssText = "max-width:78vw;max-height:78vh;object-fit:contain;"; layer.appendChild(img); document.body.appendChild(layer); } function scheduleNext(maxFirst) { var gap = GAP_MIN + Math.floor(Math.random() * (GAP_MAX - GAP_MIN)); if (maxFirst) gap = Math.min(gap, maxFirst); timer = setTimeout(flash, gap); } function flash() { if (state !== "running" || !imgs.length) return; var img = document.getElementById("subliminal-img"); if (img) { img.src = nextUrl(); layer.style.opacity = String(OPACITY); hideTimer = setTimeout(function () { if (layer) layer.style.opacity = "0"; }, FLASH_MS); } scheduleNext(); } window.Subliminal = { start: function () { // Dom-controlled, per-subject, default OFF. The experience template sets // window.SUBLIMINALS_ON from the subject's dossier; until it's true this is a // no-op no matter who calls start() (voice click, audio onplay, handoff). if (!window.SUBLIMINALS_ON) return; if (state === "running" || state === "loading") return; state = "loading"; fetch(MANIFEST, { cache: "no-store" }) .then(function (r) { return r.json(); }) .then(function (data) { if (!Array.isArray(data) || !data.length) { state = "idle"; return; } imgs = data.map(function (d) { return d.url || d; }); imgs.forEach(function (u) { var p = new Image(); p.src = u; }); // preload queue = shuffle(imgs); ensureLayer(); state = "running"; scheduleNext(FIRST_MAX); }) .catch(function () { state = "idle"; }); }, stop: function () { state = "stopped"; if (timer) { clearTimeout(timer); timer = null; } if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } if (layer) layer.style.opacity = "0"; } }; })();