/* ASCII Art Generator - Frontend */ const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const fileInfo = document.getElementById('fileInfo'); const thumbPreview = document.getElementById('thumbPreview'); const fileName = document.getElementById('fileName'); const clearFile = document.getElementById('clearFile'); const generateBtn = document.getElementById('generateBtn'); const previewArea = document.getElementById('previewArea'); const spinner = document.getElementById('spinner'); const copyBtn = document.getElementById('copyBtn'); const downloadBtn = document.getElementById('downloadBtn'); const gifIndicator = document.getElementById('gifIndicator'); const widthSlider = document.getElementById('width'); const widthVal = document.getElementById('widthVal'); let currentFile = null; let animationInterval = null; let lastRenderedHtml = ''; let zoomScale = 1.0; const zoomIn = document.getElementById('zoomIn'); const zoomOut = document.getElementById('zoomOut'); const zoomFit = document.getElementById('zoomFit'); const zoomLevel = document.getElementById('zoomLevel'); const fullscreenBtn = document.getElementById('fullscreenBtn'); const previewContainer = document.querySelector('.preview-container'); const compareBox = document.getElementById('compareBox'); const compareImg = document.getElementById('compareImg'); const compareToggle = document.getElementById('compareToggle'); // ── 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', () => { 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; } // ── Width Slider ────────────────────────────── widthSlider.addEventListener('input', () => { widthVal.textContent = widthSlider.value; }); // ── Generate ────────────────────────────── generateBtn.addEventListener('click', generate); async function generate() { 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', document.getElementById('palette').value); formData.append('bg', document.getElementById('bg').value); formData.append('dither', document.getElementById('dither').checked); formData.append('double_width', document.getElementById('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; // Check if animated (multiple frames) const frames = previewArea.querySelectorAll('.art-frame'); if (frames.length > 1) { gifIndicator.style.display = ''; startAnimation(frames); } copyBtn.style.display = ''; downloadBtn.style.display = ''; // Show original image for comparison compareImg.src = URL.createObjectURL(currentFile); compareBox.style.display = ''; compareBox.classList.remove('minimized'); compareToggle.innerHTML = '▼'; // Auto-fit to preview area requestAnimationFrame(() => { const containerWidth = previewArea.clientWidth - 24; const artWidth = previewArea.scrollWidth; if (artWidth > containerWidth) { setZoom(containerWidth / artWidth); } }); } catch (err) { spinner.style.display = 'none'; previewArea.innerHTML = `

${err.message}

`; } generateBtn.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'; const duration = parseInt(frames[current].dataset.duration) || 100; animationInterval = setTimeout(nextFrame, duration); } const duration = parseInt(frames[0].dataset.duration) || 100; animationInterval = setTimeout(nextFrame, duration); } function stopAnimation() { if (animationInterval) { clearTimeout(animationInterval); animationInterval = null; } } // ── Copy & Download ────────────────────────────── copyBtn.addEventListener('click', async () => { // Re-render as plain text for clipboard if (!currentFile) return; const formData = new FormData(); formData.append('file', currentFile); formData.append('width', widthSlider.value); formData.append('palette', document.getElementById('palette').value); formData.append('bg', document.getElementById('bg').value); formData.append('dither', document.getElementById('dither').checked); formData.append('double_width', document.getElementById('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!'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); } catch { copyBtn.textContent = 'Failed'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); } }); downloadBtn.addEventListener('click', () => { const wrapper = `
${lastRenderedHtml}
`; const blob = new Blob([wrapper], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'ascii-art.html'; a.click(); URL.revokeObjectURL(url); }); // ── Compare Box ────────────────────────────── compareToggle.addEventListener('click', e => { e.stopPropagation(); compareBox.classList.toggle('minimized'); compareToggle.innerHTML = compareBox.classList.contains('minimized') ? '▲' : '▼'; }); document.querySelector('.compare-header').addEventListener('click', () => { compareBox.classList.toggle('minimized'); compareToggle.innerHTML = compareBox.classList.contains('minimized') ? '▲' : '▼'; }); // ── Zoom ────────────────────────────── function setZoom(scale) { zoomScale = Math.max(0.25, Math.min(5.0, scale)); previewArea.style.fontSize = (8 * zoomScale) + 'px'; zoomLevel.textContent = Math.round(zoomScale * 100) + '%'; } zoomIn.addEventListener('click', () => setZoom(zoomScale + 0.15)); zoomOut.addEventListener('click', () => setZoom(zoomScale - 0.15)); zoomFit.addEventListener('click', () => { // Measure art content width vs container width const containerWidth = previewArea.clientWidth - 32; // padding // Temporarily reset to measure natural width previewArea.style.fontSize = '8px'; const artWidth = previewArea.scrollWidth; if (artWidth > 0) { const fitScale = containerWidth / artWidth; setZoom(Math.min(fitScale, 2.0)); // cap at 200% } else { setZoom(1.0); } }); // Ctrl+scroll to zoom previewArea.addEventListener('wheel', e => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; setZoom(zoomScale + delta); } }, { passive: false }); // ── Fullscreen ────────────────────────────── fullscreenBtn.addEventListener('click', toggleFullscreen); function toggleFullscreen() { previewContainer.classList.toggle('fullscreen'); if (previewContainer.classList.contains('fullscreen')) { fullscreenBtn.textContent = '\u2716'; // ✖ fullscreenBtn.title = 'Exit fullscreen (Esc)'; } else { fullscreenBtn.textContent = '\u26F6'; // ⛶ fullscreenBtn.title = 'Fullscreen (F)'; } } // ── Keyboard Shortcuts ────────────────────────────── document.addEventListener('keydown', e => { // Don't capture when typing in inputs if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; if (e.key === '+' || e.key === '=') { setZoom(zoomScale + 0.15); e.preventDefault(); } if (e.key === '-') { setZoom(zoomScale - 0.15); 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(); } });