// Fonts pre-loaded offline — no CDN needed let currentArt = ''; let letterSpacing = 0; let wrapWidth = 0; let fillChar = ''; function applyFill(art) { if (!fillChar) return art; // Replace space chars only (preserve newlines) return art.split('\n').map(line => line.replace(/ /g, fillChar)).join('\n'); } function updateFillChar(val) { fillChar = val.slice(-1); // only the last typed char if (val.length > 1) document.getElementById('fill-char-input').value = fillChar; generate(); savePrefs(); } function transformCase(mode) { const input = document.getElementById('text-input'); const t = input.value; switch (mode) { case 'upper': input.value = t.toUpperCase(); break; case 'lower': input.value = t.toLowerCase(); break; case 'title': input.value = t.replace(/\b\w/g, c => c.toUpperCase()); break; case 'toggle': input.value = t.split('').map(c => c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase()).join(''); break; } generate(); } function wordWrap(text, maxWidth) { if (!maxWidth || !text.trim()) return [text]; const words = text.split(' '); const lines = []; let cur = ''; for (const w of words) { if (cur && (cur + ' ' + w).length > maxWidth) { lines.push(cur); cur = w; } else cur = cur ? cur + ' ' + w : w; } if (cur) lines.push(cur); return lines.length ? lines : [text]; } function setLetterSpacing(n) { letterSpacing = n; document.querySelectorAll('#lspc-btns .scale-btn').forEach(btn => btn.classList.toggle('active', +btn.dataset.lspc === n) ); generate(); savePrefs(); } function updateWrapWidth(val) { wrapWidth = parseInt(val, 10); document.getElementById('wrap-width-display').textContent = wrapWidth > 0 ? wrapWidth + ' ch' : 'OFF'; generate(); savePrefs(); } function figletAsync(text, font, layout) { return new Promise((resolve, reject) => { figlet.text(text, { font, horizontalLayout: layout }, (err, result) => { if (err) reject(err); else resolve(result); }); }); } async function generate() { if (animTimer) stopAnimation(); const raw = document.getElementById('text-input').value || 'HELLO'; const font = document.getElementById('font-select').value; const output = document.getElementById('ascii-output'); const status = document.getElementById('status'); output.textContent = '...'; status.textContent = `Loading font: ${font}`; let inputLines = raw.split('\n'); if (wrapWidth > 0) inputLines = inputLines.flatMap(line => wordWrap(line, wrapWidth)); if (letterSpacing > 0) inputLines = inputLines.map(line => line.split('').join(' '.repeat(letterSpacing))); try { const rendered = await Promise.all( inputLines.map(line => line.trim() ? figletAsync(line, font, currentLayout) : Promise.resolve('')) ); currentArt = applyEcho(applyFill(applyFlip(applyBorder(alignArt(rendered.join('\n'.repeat(blockGap + 1)), currentAlign), borderStyle)))); refreshDisplay(); const artLines = currentArt.split('\n'); const lineCount = artLines.length; const maxChars = Math.max(...artLines.map(l => l.length)); const fs = parseInt(document.getElementById('font-size-slider').value, 10); const lh = Math.ceil(fs * lineHeightMult); const pad = parseInt(document.getElementById('png-padding-slider').value, 10); const estW = Math.round(maxChars * fs * 0.603 + pad * 2); const estH = Math.round(lineCount * lh + pad * 2); status.textContent = `Font: ${font} · ${lineCount} lines · ${maxChars} chars · ~${estW}×${estH}px`; const params = new URLSearchParams({ text: raw, font }); history.replaceState(null, '', '#' + params.toString()); document.title = (raw.trim().slice(0, 24) || 'ASCII') + ' — ASCII Generator'; if (!restoringHistory) pushHistory({ text: raw, font, layout: currentLayout, align: currentAlign, art: currentArt }); } catch (err) { output.textContent = `Error: ${err.message}`; status.textContent = ''; } } // ── Typewriter animation ───────────────────────────────────────────── let animTimer = null; function typewriterPlay() { if (!currentArt) return; stopAnimation(); const output = document.getElementById('ascii-output'); const btn = document.getElementById('anim-btn'); const art = currentArt; let i = 0; btn.textContent = '■ STOP'; animTimer = setInterval(() => { const speed = parseInt(document.getElementById('anim-speed').value, 10); const loop = document.getElementById('anim-loop').checked; i = Math.min(i + speed, art.length); output.textContent = art.slice(0, i); if (i >= art.length) { if (loop) { setTimeout(() => { i = 0; }, 600); } else { stopAnimation(); btn.textContent = '▶ PLAY'; } } }, 16); } function stopAnimation() { if (animTimer) { clearInterval(animTimer); animTimer = null; } const btn = document.getElementById('anim-btn'); if (btn) btn.textContent = '▶ PLAY'; if (currentArt) refreshDisplay(); } function toggleAnimation() { if (animTimer) stopAnimation(); else typewriterPlay(); } // ── Random text ────────────────────────────────────────────────────── const RANDOM_PHRASES = [ 'HELLO', 'WORLD', 'ASCII', 'RETRO', 'HACK', 'CYBER', 'NEON', 'GHOST', 'MATRIX', 'PIXEL', 'CODE', 'SUDO', 'ROOT', 'SHELL', 'BYTE', 'GLITCH', 'NOVA', 'ORBIT', 'PULSAR', 'VORTEX', 'SIGNAL', 'ECHO', 'VOID', 'APEX', 'TURBO', 'ULTRA', 'MEGA', 'HYPER', 'ZETA', 'FLUX', 'ROGUE', 'ZERO', ]; function randomText() { const pick = () => RANDOM_PHRASES[Math.floor(Math.random() * RANDOM_PHRASES.length)]; const count = Math.random() < 0.4 ? 2 : 1; const lines = Array.from({ length: count }, pick); document.getElementById('text-input').value = lines.join('\n'); generate(); } // ── Social share ───────────────────────────────────────────────────── function shareTwitter() { const url = encodeURIComponent(window.location.href); const text = encodeURIComponent('Check out my ASCII art! 🖥️'); window.open(`https://twitter.com/intent/tweet?text=${text}&url=${url}`, '_blank'); } function flash(msg) { const wrapper = document.getElementById('output-wrapper'); wrapper.dataset.flashMsg = msg; wrapper.classList.remove('flashed'); void wrapper.offsetWidth; wrapper.classList.add('flashed'); setTimeout(() => wrapper.classList.remove('flashed'), 1600); } function copyLink() { closeExportMenu(); navigator.clipboard.writeText(window.location.href).then(() => flash('COPIED LINK!')); } function copyArt() { closeExportMenu(); if (!currentArt) return; navigator.clipboard.writeText(currentArt).then(() => flash('COPIED TEXT!')); } function copyMarkdown() { closeExportMenu(); if (!currentArt) return; navigator.clipboard.writeText('```\n' + currentArt + '\n```').then(() => flash('COPIED MD!')); } function toggleTransparent(checked) { document.getElementById('png-bg').classList.toggle('disabled', checked); } function swapColors() { const prevBg = document.getElementById('preview-bg'); const prevTxt = document.getElementById('preview-text'); const pngBg = document.getElementById('png-bg'); const pngTxt = document.getElementById('png-text'); // Swap preview pair const tmp1 = prevBg.value; prevBg.value = prevTxt.value; prevTxt.value = tmp1; document.getElementById('output-wrapper').style.background = prevBg.value; document.getElementById('ascii-output').style.color = prevTxt.value; // Swap PNG pair const tmp2 = pngBg.value; pngBg.value = pngTxt.value; pngTxt.value = tmp2; flash('SWAPPED!'); } function syncToPng() { const prevBg = document.getElementById('preview-bg').value; const prevTxt = document.getElementById('preview-text').value; document.getElementById('png-bg').value = prevBg; document.getElementById('png-text').value = prevTxt; flash('SYNCED!'); } let gradDir = 'v'; let lineHeightMult = 1.15; function updateLineHeight(val) { lineHeightMult = parseFloat(val); document.getElementById('line-height-display').textContent = parseFloat(val).toFixed(2); document.getElementById('ascii-output').style.lineHeight = val; savePrefs(); } function updatePreviewGradient() { const enabled = document.getElementById('png-gradient').checked; const output = document.getElementById('ascii-output'); if (enabled) { const from = document.getElementById('grad-from').value; const to = document.getElementById('grad-to').value; const dir = gradDir === 'h' ? 'to right' : 'to bottom'; output.style.backgroundImage = `linear-gradient(${dir}, ${from}, ${to})`; output.style.backgroundClip = 'text'; output.style.webkitBackgroundClip = 'text'; output.style.color = 'transparent'; } else { output.style.backgroundImage = ''; output.style.backgroundClip = ''; output.style.webkitBackgroundClip = ''; output.style.color = document.getElementById('preview-text').value; } } function toggleGradient(checked) { document.getElementById('gradient-row').style.display = checked ? 'flex' : 'none'; updatePreviewGradient(); } function updatePreviewShadow() { const enabled = document.getElementById('png-shadow').checked; const output = document.getElementById('ascii-output'); if (enabled) { const color = document.getElementById('shadow-color').value; const blur = document.getElementById('shadow-blur').value; const offset = document.getElementById('shadow-offset').value; output.style.textShadow = `${offset}px ${offset}px ${blur}px ${color}, 0 0 ${+blur * 2}px ${color}`; } else { output.style.textShadow = ''; } } function toggleShadow(checked) { document.getElementById('shadow-row').style.display = checked ? 'flex' : 'none'; updatePreviewShadow(); } function toggleOutline(checked) { document.getElementById('outline-row').style.display = checked ? 'flex' : 'none'; } function toggleScanlines(checked) { document.getElementById('scanline-row').style.display = checked ? 'flex' : 'none'; } function toggleVignette(checked) { document.getElementById('vignette-row').style.display = checked ? 'flex' : 'none'; } function toggleBgGradient(checked) { document.getElementById('bg-gradient-row').style.display = checked ? 'flex' : 'none'; } function setGradDir(dir) { gradDir = dir; document.querySelectorAll('#grad-h-btn, #grad-v-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.graddir === dir) ); updatePreviewGradient(); savePrefs(); } function printArt() { closeExportMenu(); if (!currentArt) return; const bg = document.getElementById('preview-bg').value; const fg = document.getElementById('preview-text').value; const fs = parseInt(document.getElementById('font-size-slider').value, 10); const win = window.open('', '_blank'); win.document.write(`
${currentArt.replace(/&/g,'&').replace(//g,'>')}