const mediaInput = document.getElementById('media-input'); const dropZone = document.getElementById('drop-zone'); const results = document.getElementById('cruncher-results'); const outputFormat = document.getElementById('output-format'); const quality = document.getElementById('quality'); const qualityOutput = document.getElementById('quality-output'); const maxEdge = document.getElementById('max-edge'); const extensionByType = { 'image/webp': 'webp', 'image/jpeg': 'jpg', 'image/png': 'png' }; function formatBytes(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } function toBlob(canvas, type, imageQuality) { if ('convertToBlob' in canvas) return canvas.convertToBlob({ type, quality: imageQuality }); return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('Could not encode image')), type, imageQuality)); } function createCanvas(width, height) { if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(width, height); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } function appendStatus(name, message) { const row = document.createElement('div'); row.className = 'result-row'; const content = document.createElement('div'); const title = document.createElement('strong'); const detail = document.createElement('span'); title.textContent = name; detail.textContent = message; content.append(title, detail); row.append(content); results.append(row); } async function crunchImage(file) { const bitmap = await createImageBitmap(file); const originalWidth = bitmap.width; const originalHeight = bitmap.height; const maxSize = Math.max(320, Number(maxEdge.value) || 1920); const scale = Math.min(1, maxSize / Math.max(originalWidth, originalHeight)); const width = Math.max(1, Math.round(originalWidth * scale)); const height = Math.max(1, Math.round(originalHeight * scale)); const canvas = createCanvas(width, height); const context = canvas.getContext('2d', { alpha: outputFormat.value !== 'image/jpeg' }); context.drawImage(bitmap, 0, 0, width, height); bitmap.close(); const type = outputFormat.value; const imageQuality = type === 'image/png' ? undefined : Number(quality.value) / 100; const blob = await toBlob(canvas, type, imageQuality); const url = URL.createObjectURL(blob); const saved = file.size - blob.size; const ratio = file.size > 0 ? Math.round((saved / file.size) * 100) : 0; const row = document.createElement('div'); const content = document.createElement('div'); const name = document.createElement('strong'); const dimensions = document.createElement('span'); const sizes = document.createElement('span'); const download = document.createElement('a'); row.className = 'result-row'; name.textContent = file.name; dimensions.textContent = `${originalWidth}x${originalHeight} -> ${width}x${height}`; sizes.textContent = `${formatBytes(file.size)} -> ${formatBytes(blob.size)} (${saved >= 0 ? ratio + '% smaller' : Math.abs(ratio) + '% larger'})`; download.className = 'download-link'; download.download = `${file.name.replace(/\.[^.]+$/, '')}-crunched.${extensionByType[type] || 'bin'}`; download.href = url; download.textContent = 'Download'; content.append(name, dimensions, sizes); row.append(content, download); results.append(row); } async function handleFiles(files) { results.innerHTML = ''; for (const file of files) { if (file.type.startsWith('image/')) { try { await crunchImage(file); } catch (error) { appendStatus(file.name, error.message); } } else if (file.type.startsWith('video/')) { appendStatus(file.name, 'Video accepted as a future Wasm codec target. Nothing was uploaded.'); } else { appendStatus(file.name, 'Unsupported file type.'); } } } quality.addEventListener('input', () => { qualityOutput.textContent = `${quality.value}%`; }); mediaInput.addEventListener('change', () => handleFiles(mediaInput.files)); ['dragenter', 'dragover'].forEach((eventName) => dropZone.addEventListener(eventName, (event) => { event.preventDefault(); dropZone.classList.add('is-dragging'); })); ['dragleave', 'drop'].forEach((eventName) => dropZone.addEventListener(eventName, (event) => { event.preventDefault(); dropZone.classList.remove('is-dragging'); })); dropZone.addEventListener('drop', (event) => handleFiles(event.dataTransfer.files));