/* ASCII Art Generator - Frontend */ const $ = id => document.getElementById(id); const dropZone = $('dropZone'); const fileInput = $('fileInput'); const fileInfo = $('fileInfo'); const thumbPreview = $('thumbPreview'); const fileName = $('fileName'); const clearFile = $('clearFile'); const generateBtn = $('generateBtn'); const randomBtn = $('randomBtn'); const previewArea = $('previewArea'); const spinner = $('spinner'); const copyBtn = $('copyBtn'); const downloadBtn = $('downloadBtn'); const gifIndicator = $('gifIndicator'); const widthSlider = $('width'); const widthVal = $('widthVal'); const zoomInBtn = $('zoomIn'); const zoomOutBtn = $('zoomOut'); const zoomFitBtn = $('zoomFit'); const zoomLevel = $('zoomLevel'); const fullscreenBtn = $('fullscreenBtn'); const previewContainer = document.querySelector('.preview-container'); const compareBox = $('compareBox'); const compareImg = $('compareImg'); const compareToggle = $('compareToggle'); const imageControls = $('imageControls'); const patternControls = $('patternControls'); const patternHeight = $('patternHeight'); const heightVal = $('heightVal'); let currentFile = null; let animationInterval = null; let lastRenderedHtml = ''; let zoomScale = 1.0; let currentMode = 'image'; // ── Mode Switching ────────────────────────────── const modeImage = $('modeImage'); const modePattern = $('modePattern'); function switchMode(mode) { currentMode = mode; modeImage.classList.toggle('active', mode === 'image'); modePattern.classList.toggle('active', mode === 'pattern'); imageControls.style.display = mode === 'image' ? 'flex' : 'none'; patternControls.style.display = mode === 'pattern' ? 'flex' : 'none'; generateBtn.style.display = mode === 'image' ? '' : 'none'; randomBtn.style.display = mode === 'pattern' ? '' : 'none'; if (mode === 'image') { generateBtn.disabled = !currentFile; } } modeImage.addEventListener('click', () => switchMode('image')); modePattern.addEventListener('click', () => switchMode('pattern')); // ── File Handling ────────────────────────────── dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length) setFile(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', () => { if (fileInput.files.length) setFile(fileInput.files[0]); }); clearFile.addEventListener('click', e => { e.stopPropagation(); currentFile = null; fileInput.value = ''; fileInfo.style.display = 'none'; dropZone.style.display = ''; generateBtn.disabled = true; }); function setFile(file) { const allowed = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; if (!allowed.includes(file.type)) { alert('Please upload a PNG, JPG, GIF, or WebP image.'); return; } if (file.size > 10 * 1024 * 1024) { alert('File too large (max 10MB).'); return; } currentFile = file; fileName.textContent = file.name; thumbPreview.src = URL.createObjectURL(file); dropZone.style.display = 'none'; fileInfo.style.display = 'flex'; generateBtn.disabled = false; } // ── Sliders ────────────────────────────── widthSlider.addEventListener('input', () => { widthVal.textContent = widthSlider.value; }); patternHeight.addEventListener('input', () => { heightVal.textContent = patternHeight.value; }); // Resolution presets sync the width slider const resPreset = $('resPreset'); resPreset.addEventListener('change', () => { const val = parseInt(resPreset.value); widthSlider.value = val; widthVal.textContent = val; }); widthSlider.addEventListener('input', () => { // Update preset dropdown to match closest const val = parseInt(widthSlider.value); const opts = [100, 300, 600, 1000, 1500]; let closest = opts[0]; for (const o of opts) { if (Math.abs(o - val) < Math.abs(closest - val)) closest = o; } resPreset.value = closest; }); // ── Generate (Image Mode) ────────────────────────────── generateBtn.addEventListener('click', generateImage); async function generateImage() { if (!currentFile) return; stopAnimation(); previewArea.innerHTML = ''; spinner.style.display = 'flex'; generateBtn.disabled = true; copyBtn.style.display = 'none'; downloadBtn.style.display = 'none'; gifIndicator.style.display = 'none'; const formData = new FormData(); formData.append('file', currentFile); formData.append('width', widthSlider.value); formData.append('palette', $('palette').value); formData.append('bg', $('bg').value); formData.append('dither', $('dither').checked); formData.append('double_width', $('doubleWidth').checked); formData.append('output_format', 'html'); try { const resp = await fetch('/api/render', { method: 'POST', body: formData }); if (!resp.ok) { const err = await resp.json().catch(() => ({ error: 'Request failed' })); throw new Error(err.error || `HTTP ${resp.status}`); } const html = await resp.text(); lastRenderedHtml = html; spinner.style.display = 'none'; previewArea.innerHTML = html; const frames = previewArea.querySelectorAll('.art-frame'); if (frames.length > 1) { gifIndicator.style.display = ''; startAnimation(frames); } copyBtn.style.display = ''; downloadBtn.style.display = ''; // Show original for comparison compareImg.src = URL.createObjectURL(currentFile); compareBox.style.display = ''; compareBox.classList.remove('minimized'); compareToggle.innerHTML = '▼'; autoFitZoom(); } catch (err) { spinner.style.display = 'none'; previewArea.innerHTML = `

${err.message}

`; } generateBtn.disabled = false; } // ── Generate (Pattern Mode) ────────────────────────────── randomBtn.addEventListener('click', generatePattern); async function generatePattern() { stopAnimation(); previewArea.innerHTML = ''; spinner.style.display = 'flex'; randomBtn.disabled = true; copyBtn.style.display = 'none'; downloadBtn.style.display = 'none'; compareBox.style.display = 'none'; let patternType = $('patternType').value; if (patternType === 'random') { const opts = [...$('patternType').options].map(o => o.value).filter(v => v !== 'random'); patternType = opts[Math.floor(Math.random() * opts.length)]; } const formData = new FormData(); formData.append('pattern', patternType); formData.append('width', widthSlider.value); formData.append('height', patternHeight.value); formData.append('palette', $('palette').value); formData.append('output_format', 'html'); try { const resp = await fetch('/api/pattern', { method: 'POST', body: formData }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const html = await resp.text(); lastRenderedHtml = html; spinner.style.display = 'none'; previewArea.innerHTML = html; copyBtn.style.display = ''; downloadBtn.style.display = ''; autoFitZoom(); } catch (err) { spinner.style.display = 'none'; previewArea.innerHTML = `

${err.message}

`; } randomBtn.disabled = false; } // ── GIF Animation ────────────────────────────── function startAnimation(frames) { let current = 0; frames[0].style.display = 'block'; function nextFrame() { frames[current].style.display = 'none'; current = (current + 1) % frames.length; frames[current].style.display = 'block'; animationInterval = setTimeout(nextFrame, parseInt(frames[current].dataset.duration) || 100); } animationInterval = setTimeout(nextFrame, parseInt(frames[0].dataset.duration) || 100); } function stopAnimation() { if (animationInterval) { clearTimeout(animationInterval); animationInterval = null; } } // ── Copy & Download ────────────────────────────── copyBtn.addEventListener('click', async () => { // For patterns, just extract text from the current render if (currentMode === 'pattern') { const text = previewArea.textContent; try { await navigator.clipboard.writeText(text); copyBtn.textContent = 'Copied!'; } catch { copyBtn.textContent = 'Failed'; } setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); return; } if (!currentFile) return; const formData = new FormData(); formData.append('file', currentFile); formData.append('width', widthSlider.value); formData.append('palette', $('palette').value); formData.append('bg', $('bg').value); formData.append('dither', $('dither').checked); formData.append('double_width', $('doubleWidth').checked); formData.append('output_format', 'plain'); try { const resp = await fetch('/api/render', { method: 'POST', body: formData }); const text = await resp.text(); await navigator.clipboard.writeText(text); copyBtn.textContent = 'Copied!'; } catch { copyBtn.textContent = 'Failed'; } setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); }); downloadBtn.addEventListener('click', () => { const wrapper = `
${lastRenderedHtml}
`; const blob = new Blob([wrapper], { type: 'text/html' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'ascii-art.html'; a.click(); URL.revokeObjectURL(a.href); }); // ── Compare Box ────────────────────────────── compareToggle.addEventListener('click', e => { e.stopPropagation(); toggleCompare(); }); document.querySelector('.compare-header').addEventListener('click', toggleCompare); function toggleCompare() { compareBox.classList.toggle('minimized'); compareToggle.innerHTML = compareBox.classList.contains('minimized') ? '▲' : '▼'; } // ── Zoom ────────────────────────────── function setZoom(scale) { zoomScale = Math.max(0.1, Math.min(8.0, scale)); previewArea.style.fontSize = (5 * zoomScale) + 'px'; zoomLevel.textContent = Math.round(zoomScale * 100) + '%'; } function autoFitZoom() { requestAnimationFrame(() => { const containerWidth = previewArea.clientWidth - 24; previewArea.style.fontSize = '5px'; zoomScale = 1.0; const artWidth = previewArea.scrollWidth; if (artWidth > containerWidth && artWidth > 0) { setZoom(containerWidth / artWidth); } else { zoomLevel.textContent = '100%'; } }); } zoomInBtn.addEventListener('click', () => setZoom(zoomScale * 1.2)); zoomOutBtn.addEventListener('click', () => setZoom(zoomScale / 1.2)); zoomFitBtn.addEventListener('click', autoFitZoom); // Mouse wheel zoom (Ctrl/Cmd + scroll, or pinch gesture which browsers send as ctrlKey wheel) previewArea.addEventListener('wheel', e => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const factor = e.deltaY > 0 ? 0.9 : 1.1; setZoom(zoomScale * factor); } }, { passive: false }); // ── Touch Pinch-to-Zoom ────────────────────────────── let touchStartDist = 0; let touchStartZoom = 1.0; previewArea.addEventListener('touchstart', e => { if (e.touches.length === 2) { e.preventDefault(); const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; touchStartDist = Math.sqrt(dx * dx + dy * dy); touchStartZoom = zoomScale; } }, { passive: false }); previewArea.addEventListener('touchmove', e => { if (e.touches.length === 2) { e.preventDefault(); const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const dist = Math.sqrt(dx * dx + dy * dy); if (touchStartDist > 0) { setZoom(touchStartZoom * (dist / touchStartDist)); } } }, { passive: false }); previewArea.addEventListener('touchend', () => { touchStartDist = 0; }); // ── Fullscreen ────────────────────────────── fullscreenBtn.addEventListener('click', toggleFullscreen); function toggleFullscreen() { previewContainer.classList.toggle('fullscreen'); fullscreenBtn.textContent = previewContainer.classList.contains('fullscreen') ? '\u2716' : '\u26F6'; fullscreenBtn.title = previewContainer.classList.contains('fullscreen') ? 'Exit (Esc)' : 'Fullscreen (F)'; } // ── Keyboard Shortcuts ────────────────────────────── document.addEventListener('keydown', e => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return; if (e.key === '+' || e.key === '=') { setZoom(zoomScale * 1.2); e.preventDefault(); } if (e.key === '-') { setZoom(zoomScale / 1.2); e.preventDefault(); } if (e.key === '0') { setZoom(1.0); e.preventDefault(); } if (e.key === 'f' || e.key === 'F') { toggleFullscreen(); e.preventDefault(); } if (e.key === 'Escape' && previewContainer.classList.contains('fullscreen')) { toggleFullscreen(); e.preventDefault(); } });