/* asciigen frontend — vanilla JS, no dependencies. */ (function () { "use strict"; var artEl = document.getElementById("art"); var scrollEl = document.getElementById("artScroll"); var stTitle = document.getElementById("stTitle"); var stDims = document.getElementById("stDims"); var stMsg = document.getElementById("stMsg"); var genBtn = document.getElementById("genBtn"); var copyBtn = document.getElementById("copyBtn"); var copyLabel = document.getElementById("copyLabel"); var current = window.__initial || { art: artEl.textContent, title: "", cols: 0, rows: 0 }; var activeCat = "any"; var busy = false; var msgTimer = null; var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); /* --------------------------------------------------------------------- Fit-to-width: scale art font down so the widest line fits the frame, flooring at a readable minimum (horizontal scroll takes over below). --------------------------------------------------------------------- */ var ART_BASE = 15; var ART_MIN = 7; function fitArt() { artEl.style.fontSize = ART_BASE + "px"; var avail = scrollEl.clientWidth; var need = artEl.scrollWidth; if (need > avail && need > 0) { var size = Math.max(ART_MIN, Math.floor(ART_BASE * (avail / need) * 10) / 10); artEl.style.fontSize = size + "px"; } } var resizeTimer = null; window.addEventListener("resize", function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(fitArt, 120); }); /* --------------------------------------------------------------------- Scramble-resolve transition. Whitespace stays fixed so the silhouette appears instantly; visible characters resolve at staggered times. --------------------------------------------------------------------- */ var GLYPHS = "#@%&$?!<>[]{}/\\|=+*~^;:"; function setArt(text) { artEl.textContent = text; fitArt(); if (reduceMotion.matches) { return Promise.resolve(); } var chars = Array.from(text); var resolveAt = chars.map(function (c) { return c === "\n" || c === " " ? 0 : 120 + Math.random() * 480; }); var t0 = performance.now(); return new Promise(function (done) { function frame(t) { var dt = t - t0; var out = ""; var pending = false; for (var i = 0; i < chars.length; i++) { var c = chars[i]; if (c === "\n" || c === " " || dt >= resolveAt[i]) { out += c; } else { pending = true; out += GLYPHS[(Math.random() * GLYPHS.length) | 0]; } } artEl.textContent = out; if (pending) { requestAnimationFrame(frame); } else { artEl.textContent = text; done(); } } requestAnimationFrame(frame); }); } /* --------------------------------------------------------------------- Status readout --------------------------------------------------------------------- */ function msg(text, hot) { clearTimeout(msgTimer); stMsg.textContent = text; stMsg.classList.toggle("is-hot", !!hot); if (hot) { msgTimer = setTimeout(function () { stMsg.textContent = "ready"; stMsg.classList.remove("is-hot"); }, 1600); } } /* --------------------------------------------------------------------- Generate --------------------------------------------------------------------- */ function generate() { if (busy) return; busy = true; genBtn.disabled = true; msg("tuning…", true); var q = activeCat === "any" ? "" : "?category=" + encodeURIComponent(activeCat); fetch("/api/random" + q) .then(function (r) { if (!r.ok) throw new Error("http " + r.status); return r.json(); }) .then(function (d) { current = d; stTitle.textContent = d.title; stDims.textContent = d.cols + "×" + d.rows; return setArt(d.art); }) .then(function () { msg("ready"); }) .catch(function () { msg("signal lost", true); }) .then(function () { busy = false; genBtn.disabled = false; }); } /* --------------------------------------------------------------------- Copy to clipboard (with execCommand fallback for non-secure contexts) --------------------------------------------------------------------- */ function copyArt() { var text = current.art || artEl.textContent; var ok = function () { copyLabel.textContent = "copied ✓"; msg("copied", true); setTimeout(function () { copyLabel.textContent = "copy"; }, 1400); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(ok, function () { fallbackCopy(text, ok); }); } else { fallbackCopy(text, ok); } } function fallbackCopy(text, ok) { var ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); ok(); } catch (e) { msg("copy failed", true); } document.body.removeChild(ta); } /* --------------------------------------------------------------------- Categories --------------------------------------------------------------------- */ var catBtns = Array.prototype.slice.call(document.querySelectorAll(".cat")); catBtns.forEach(function (btn) { btn.addEventListener("click", function () { activeCat = btn.dataset.cat; catBtns.forEach(function (b) { b.classList.toggle("is-active", b === btn); }); generate(); }); }); /* --------------------------------------------------------------------- Themes --------------------------------------------------------------------- */ var THEMES = ["graphite", "paper", "phosphor", "amber"]; var themeBtns = Array.prototype.slice.call(document.querySelectorAll(".theme-btn")); function applyTheme(name) { document.documentElement.dataset.theme = name; try { localStorage.setItem("asciigen.theme", name); } catch (e) {} themeBtns.forEach(function (b) { b.classList.toggle("is-active", b.dataset.setTheme === name); }); } themeBtns.forEach(function (btn) { btn.addEventListener("click", function () { applyTheme(btn.dataset.setTheme); }); }); function cycleTheme() { var cur = document.documentElement.dataset.theme; var idx = THEMES.indexOf(cur); applyTheme(THEMES[(idx + 1) % THEMES.length]); } /* mark the boot theme's button as active */ applyTheme(document.documentElement.dataset.theme || "graphite"); /* --------------------------------------------------------------------- Keyboard: space = generate, c = copy, t = cycle theme --------------------------------------------------------------------- */ document.addEventListener("keydown", function (e) { if (e.repeat || e.metaKey || e.ctrlKey || e.altKey) return; var tag = (e.target.tagName || "").toLowerCase(); var interactive = tag === "button" || tag === "input" || tag === "textarea" || tag === "select"; if (e.code === "Space" && !interactive) { e.preventDefault(); generate(); } else if (e.key === "c" && !interactive) { copyArt(); } else if (e.key === "t" && !interactive) { cycleTheme(); } }); genBtn.addEventListener("click", generate); copyBtn.addEventListener("click", copyArt); fitArt(); })();