diff --git a/static/app.js b/static/app.js index cfada6b..d517fcc 100644 --- a/static/app.js +++ b/static/app.js @@ -1,48 +1,79 @@ /* 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'); +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'; // 'image' or 'pattern' +let currentMode = 'image'; -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'); +// ── Mode Switching ────────────────────────────── + +document.querySelectorAll('.mode-tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + currentMode = tab.dataset.mode; + + if (currentMode === 'pattern') { + imageControls.style.display = 'none'; + patternControls.style.display = 'flex'; + generateBtn.style.display = 'none'; + randomBtn.style.display = ''; + } else { + imageControls.style.display = 'flex'; + patternControls.style.display = 'none'; + generateBtn.style.display = ''; + generateBtn.disabled = !currentFile; + randomBtn.style.display = 'none'; + } + }); +}); // ── File Handling ────────────────────────────── -dropZone.addEventListener('click', () => fileInput.click()); +// Only open file picker from the dropZone click — prevent double-trigger from label +dropZone.addEventListener('click', e => { + e.preventDefault(); + e.stopPropagation(); + fileInput.click(); +}); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); }); -dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('dragover'); -}); +dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', e => { e.preventDefault(); @@ -54,7 +85,8 @@ fileInput.addEventListener('change', () => { if (fileInput.files.length) setFile(fileInput.files[0]); }); -clearFile.addEventListener('click', () => { +clearFile.addEventListener('click', e => { + e.stopPropagation(); currentFile = null; fileInput.value = ''; fileInfo.style.display = 'none'; @@ -72,7 +104,6 @@ function setFile(file) { alert('File too large (max 10MB).'); return; } - currentFile = file; fileName.textContent = file.name; thumbPreview.src = URL.createObjectURL(file); @@ -81,38 +112,69 @@ function setFile(file) { generateBtn.disabled = false; } -// ── Mode Switching ────────────────────────────── +// ── Sliders ────────────────────────────── -const modeTabs = document.querySelectorAll('.mode-tab'); -const imageControls = document.getElementById('imageControls'); -const patternControls = document.getElementById('patternControls'); -const randomBtn = document.getElementById('randomBtn'); -const patternHeight = document.getElementById('patternHeight'); -const heightVal = document.getElementById('heightVal'); +widthSlider.addEventListener('input', () => { widthVal.textContent = widthSlider.value; }); +patternHeight.addEventListener('input', () => { heightVal.textContent = patternHeight.value; }); -modeTabs.forEach(tab => { - tab.addEventListener('click', () => { - modeTabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - currentMode = tab.dataset.mode; +// ── Generate (Image Mode) ────────────────────────────── - if (currentMode === 'pattern') { - imageControls.style.display = 'none'; - patternControls.style.display = 'flex'; - generateBtn.style.display = 'none'; - randomBtn.style.display = ''; - } else { - imageControls.style.display = 'flex'; - patternControls.style.display = 'none'; - generateBtn.style.display = ''; - randomBtn.style.display = 'none'; +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; -patternHeight.addEventListener('input', () => { - heightVal.textContent = patternHeight.value; -}); + 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); @@ -125,18 +187,17 @@ async function generatePattern() { downloadBtn.style.display = 'none'; compareBox.style.display = 'none'; - let patternType = document.getElementById('patternType').value; + let patternType = $('patternType').value; if (patternType === 'random') { - const types = [...document.getElementById('patternType').options] - .map(o => o.value).filter(v => v !== 'random'); - patternType = types[Math.floor(Math.random() * types.length)]; + 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', document.getElementById('palette').value); + formData.append('palette', $('palette').value); formData.append('output_format', 'html'); try { @@ -146,18 +207,9 @@ async function generatePattern() { lastRenderedHtml = html; spinner.style.display = 'none'; previewArea.innerHTML = html; - copyBtn.style.display = ''; downloadBtn.style.display = ''; - - // Auto-fit - requestAnimationFrame(() => { - const containerWidth = previewArea.clientWidth - 24; - const artWidth = previewArea.scrollWidth; - if (artWidth > containerWidth) { - setZoom(containerWidth / artWidth); - } - }); + autoFitZoom(); } catch (err) { spinner.style.display = 'none'; previewArea.innerHTML = `${err.message}
`; @@ -165,118 +217,46 @@ async function generatePattern() { randomBtn.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); + animationInterval = setTimeout(nextFrame, parseInt(frames[current].dataset.duration) || 100); } - - const duration = parseInt(frames[0].dataset.duration) || 100; - animationInterval = setTimeout(nextFrame, duration); + animationInterval = setTimeout(nextFrame, parseInt(frames[0].dataset.duration) || 100); } function stopAnimation() { - if (animationInterval) { - clearTimeout(animationInterval); - animationInterval = null; - } + if (animationInterval) { clearTimeout(animationInterval); animationInterval = null; } } // ── Copy & Download ────────────────────────────── copyBtn.addEventListener('click', async () => { - // Re-render as plain text for clipboard - if (!currentFile) return; + // 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', 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('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 { @@ -284,11 +264,8 @@ copyBtn.addEventListener('click', async () => { 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); - } + } catch { copyBtn.textContent = 'Failed'; } + setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); }); downloadBtn.addEventListener('click', () => { @@ -297,90 +274,112 @@ downloadBtn.addEventListener('click', () => { body{background:#0d1117;margin:20px;display:flex;justify-content:center} pre{font-family:'JetBrains Mono',monospace;font-size:11px;line-height:1.15;letter-spacing:0.5px}${lastRenderedHtml}