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}
`; - const blob = new Blob([wrapper], { type: 'text/html' }); - const url = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; + a.href = URL.createObjectURL(blob); a.download = 'ascii-art.html'; a.click(); - URL.revokeObjectURL(url); + URL.revokeObjectURL(a.href); }); // ── Compare Box ────────────────────────────── compareToggle.addEventListener('click', e => { e.stopPropagation(); - compareBox.classList.toggle('minimized'); - compareToggle.innerHTML = compareBox.classList.contains('minimized') ? '▲' : '▼'; + toggleCompare(); }); -document.querySelector('.compare-header').addEventListener('click', () => { +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.25, Math.min(5.0, scale)); + zoomScale = Math.max(0.1, Math.min(8.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)); +function autoFitZoom() { + requestAnimationFrame(() => { + const containerWidth = previewArea.clientWidth - 24; + previewArea.style.fontSize = '8px'; + zoomScale = 1.0; + const artWidth = previewArea.scrollWidth; + if (artWidth > containerWidth && artWidth > 0) { + setZoom(containerWidth / artWidth); + } else { + zoomLevel.textContent = '100%'; + } + }); +} -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); - } -}); +zoomInBtn.addEventListener('click', () => setZoom(zoomScale * 1.2)); +zoomOutBtn.addEventListener('click', () => setZoom(zoomScale / 1.2)); +zoomFitBtn.addEventListener('click', autoFitZoom); -// Ctrl+scroll to zoom +// 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 delta = e.deltaY > 0 ? -0.1 : 0.1; - setZoom(zoomScale + delta); + 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'); - if (previewContainer.classList.contains('fullscreen')) { - fullscreenBtn.textContent = '\u2716'; // ✖ - fullscreenBtn.title = 'Exit fullscreen (Esc)'; - } else { - fullscreenBtn.textContent = '\u26F6'; // ⛶ - fullscreenBtn.title = 'Fullscreen (F)'; - } + fullscreenBtn.textContent = previewContainer.classList.contains('fullscreen') ? '\u2716' : '\u26F6'; + fullscreenBtn.title = previewContainer.classList.contains('fullscreen') ? 'Exit (Esc)' : '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.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return; - if (e.key === '+' || e.key === '=') { setZoom(zoomScale + 0.15); e.preventDefault(); } - if (e.key === '-') { setZoom(zoomScale - 0.15); e.preventDefault(); } + 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(); + toggleFullscreen(); e.preventDefault(); } });