ascii-art/static/app.js

387 lines
13 KiB
JavaScript

/* 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;
let currentMode = 'image'; // 'image' or 'pattern'
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;
}
// ── Mode Switching ──────────────────────────────
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');
modeTabs.forEach(tab => {
tab.addEventListener('click', () => {
modeTabs.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 = '';
randomBtn.style.display = 'none';
}
});
});
patternHeight.addEventListener('input', () => {
heightVal.textContent = patternHeight.value;
});
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 = document.getElementById('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 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('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 = '';
// Auto-fit
requestAnimationFrame(() => {
const containerWidth = previewArea.clientWidth - 24;
const artWidth = previewArea.scrollWidth;
if (artWidth > containerWidth) {
setZoom(containerWidth / artWidth);
}
});
} catch (err) {
spinner.style.display = 'none';
previewArea.innerHTML = `<p class="placeholder" style="color:#ff453a">${err.message}</p>`;
}
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 = '&#x25BC;';
// 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 = `<p class="placeholder" style="color:#ff453a">${err.message}</p>`;
}
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 = `<!DOCTYPE html>
<html><head><meta charset="UTF-8"><style>
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}
</style></head><body><pre>${lastRenderedHtml}</pre></body></html>`;
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') ? '&#x25B2;' : '&#x25BC;';
});
document.querySelector('.compare-header').addEventListener('click', () => {
compareBox.classList.toggle('minimized');
compareToggle.innerHTML = compareBox.classList.contains('minimized') ? '&#x25B2;' : '&#x25BC;';
});
// ── 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();
}
});