'use strict'; /* ================================================================ i2t OCR — app.js v3 FIXES vs previous versions: 1. Engine tabs: simple direct classList toggle, no wrapper objects 2. Tesseract: tested createWorker API for tesseract.js v5 3. OCR Cloud: uses /ocr/sync endpoint with polling fallback 4. Loading overlay: covers workspace-panels via CSS position:absolute 5. File input: pointer-events fixed so click always reaches input ================================================================ */ // ─── Config ──────────────────────────────────────────────────────── var API_BASE = 'https://i2tocr.com'; // change to your server URL var POLL_MS = 800; // polling interval for async results // ─── State ───────────────────────────────────────────────────────── var currentEngine = 'tesseract'; var currentFile = null; // ─── Tiny DOM helper ──────────────────────────────────────────────── function g(id) { return document.getElementById(id); } // ════════════════════════════════════════════════════════════════════ // ENGINE TABS // Simple, direct — no wrappers that can go null // ════════════════════════════════════════════════════════════════════ function activateTab(engine) { currentEngine = engine; var tTess = g('tab-tesseract'); var tCloud = g('tab-ocrcloud'); var badge = g('engine-badge'); if (!tTess || !tCloud) return; // Remove active classes from both tTess.className = 'engine-tab'; tCloud.className = 'engine-tab'; if (engine === 'tesseract') { tTess.className = 'engine-tab active-teal'; if (badge) { badge.textContent = 'Tesseract.js'; badge.className = 'ocr-engine-badge teal'; } } else { tCloud.className = 'engine-tab active-purple'; if (badge) { badge.textContent = 'OCR Cloud'; badge.className = 'ocr-engine-badge purple'; } } } // ════════════════════════════════════════════════════════════════════ // LOADING OVERLAY // ════════════════════════════════════════════════════════════════════ function setLoading(on, msg) { var ov = g('ocr-loading'); var txt = g('loading-text'); var btn = g('btn-run-ocr'); if (!ov) return; if (on) { ov.classList.add('visible'); if (txt && msg) txt.textContent = msg; } else { ov.classList.remove('visible'); if (txt) txt.textContent = ''; } if (btn) btn.disabled = on; } // ════════════════════════════════════════════════════════════════════ // CHAR COUNT // ════════════════════════════════════════════════════════════════════ function updateCount() { var ta = g('text-output'); var cc = g('char-count'); if (!ta || !cc) return; var len = ta.value.length; var words = len ? ta.value.trim().split(/\s+/).filter(Boolean).length : 0; cc.textContent = len ? len + ' chars · ' + words + ' words' : ''; } // ════════════════════════════════════════════════════════════════════ // SHOW WORKSPACE after file selected // ════════════════════════════════════════════════════════════════════ function showWorkspace(file) { var reader = new FileReader(); reader.onload = function(e) { var wrap = g('img-preview'); if (wrap) wrap.innerHTML = 'preview'; }; reader.readAsDataURL(file); var ua = g('upload-area'); var ws = g('workspace'); var ta = g('text-output'); if (ua) ua.style.display = 'none'; if (ws) ws.classList.add('visible'); if (ta) { ta.value = ''; updateCount(); } } // ════════════════════════════════════════════════════════════════════ // TESSERACT.JS (browser-side) // ════════════════════════════════════════════════════════════════════ async function runTesseract(file, lang) { if (typeof Tesseract === 'undefined') { throw new Error('Tesseract.js did not load — check your internet connection'); } // tesseract.js v5: createWorker(lang, oem, options) var worker = await Tesseract.createWorker(lang, 1, { logger: function(m) { var txt = g('loading-text'); if (!txt) return; switch (m.status) { case 'loading tesseract core': txt.textContent = 'Loading engine…'; break; case 'initializing tesseract': txt.textContent = 'Initializing…'; break; case 'loading language traineddata': txt.textContent = 'Loading language data…'; break; case 'initializing api': txt.textContent = 'Preparing…'; break; case 'recognizing text': txt.textContent = 'Recognizing… ' + Math.round((m.progress || 0) * 100) + '%'; break; } }, }); var result = await worker.recognize(file); await worker.terminate(); return result.data.text; } // ════════════════════════════════════════════════════════════════════ // OCR CLOUD (server-side Tesseract via FastAPI) // Uses /ocr/sync — if it times out, polls /ocr/{task_id} // ════════════════════════════════════════════════════════════════════ async function runOcrCloud(file, lang) { var form = new FormData(); form.append('file', file); form.append('lang', lang); form.append('config', '--psm 4'); setLoading(true, 'Sending to server…'); var res = await fetch(API_BASE + '/ocr/sync', { method: 'POST', body: form }); var data = await res.json(); if (!res.ok) { throw new Error(data.detail || 'HTTP ' + res.status); } // Success on first try if (data.status === 'success') { return data.data.text; } // Server is still processing — poll if (data.status === 'processing' && data.task_id) { return await pollTask(data.task_id); } throw new Error(data.detail || 'Unexpected response from server'); } async function pollTask(taskId) { var maxAttempts = 60; // 60 × 800 ms = 48 s max for (var i = 0; i < maxAttempts; i++) { setLoading(true, 'Processing… (' + (i + 1) + 's)'); await sleep(POLL_MS); var res = await fetch(API_BASE + '/ocr/' + taskId); var data = await res.json(); if (data.status === 'success') return data.data.text; if (data.status === 'failure') throw new Error(data.detail || 'OCR failed on server'); // still 'processing' → keep looping } throw new Error('OCR timed out — please try a smaller image'); } function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } // ════════════════════════════════════════════════════════════════════ // MAIN OCR RUNNER // ════════════════════════════════════════════════════════════════════ async function runOcr(lang) { if (!currentFile) return; setLoading(true, 'Starting…'); var ta = g('text-output'); if (ta) { ta.value = ''; updateCount(); } try { var text; if (currentEngine === 'tesseract') { text = await runTesseract(currentFile, lang); } else { text = await runOcrCloud(currentFile, lang); } if (ta) { ta.value = (text || '').trim(); updateCount(); } } catch (err) { console.error('[OCR error]', err); if (ta) { ta.value = '⚠ Error: ' + err.message; updateCount(); } } finally { setLoading(false); } } // ════════════════════════════════════════════════════════════════════ // LANGUAGE MODAL // ════════════════════════════════════════════════════════════════════ function openModal() { var sel = g('lang-select'); var ov = g('modal-overlay'); if (sel) sel.value = ''; if (ov) ov.classList.add('open'); setTimeout(function() { if (sel) sel.focus(); }, 150); } function closeModal() { var ov = g('modal-overlay'); if (ov) ov.classList.remove('open'); } // ════════════════════════════════════════════════════════════════════ // FILE HANDLING // ════════════════════════════════════════════════════════════════════ function handleFile(file) { if (!file) return; if (!file.type.startsWith('image/')) { alert('Please select an image file (JPG, PNG, WEBP, etc.)'); return; } currentFile = file; showWorkspace(file); // Auto-run immediately: // Tesseract → English by default (user can re-run with different lang) // Cloud → must pick language first if (currentEngine === 'tesseract') { setTimeout(function() { runOcr('eng'); }, 200); } else { openModal(); } } // ════════════════════════════════════════════════════════════════════ // PARTICLES (background animation) // ════════════════════════════════════════════════════════════════════ function initParticles() { var canvas = g('particles-canvas'); if (!canvas) return; var ctx = canvas.getContext('2d'); var W, H, P = []; function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } function mkP() { return { x: Math.random() * W, y: Math.random() * H, r: Math.random() * 1.4 + 0.3, vx: (Math.random() - 0.5) * 0.22, vy: (Math.random() - 0.5) * 0.22, a: Math.random() * 0.4 + 0.1, c: Math.random() > 0.55 ? '#6c5ce7' : '#00cec9', }; } resize(); window.addEventListener('resize', resize, { passive: true }); for (var i = 0; i < 80; i++) P.push(mkP()); (function draw() { ctx.clearRect(0, 0, W, H); for (var i = 0; i < P.length; i++) { var a = P[i]; for (var j = i + 1; j < P.length; j++) { var b = P[j], d = Math.hypot(a.x - b.x, a.y - b.y); if (d < 110) { ctx.beginPath(); ctx.strokeStyle = 'rgba(108,92,231,' + (0.06 * (1 - d / 110)) + ')'; ctx.lineWidth = 0.5; ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } P.forEach(function(p) { p.x += p.vx; p.y += p.vy; if (p.x < 0) p.x = W; if (p.x > W) p.x = 0; if (p.y < 0) p.y = H; if (p.y > H) p.y = 0; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = p.c; ctx.globalAlpha = p.a; ctx.fill(); ctx.globalAlpha = 1; }); requestAnimationFrame(draw); }()); } // ════════════════════════════════════════════════════════════════════ // SCROLL REVEAL // ════════════════════════════════════════════════════════════════════ function initReveal() { var obs = new IntersectionObserver(function(entries) { entries.forEach(function(e) { if (e.isIntersecting) { e.target.style.opacity = '1'; e.target.style.transform = 'translateY(0)'; obs.unobserve(e.target); } }); }, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }); document.querySelectorAll('.feature-card, .step-card, .orbit-card').forEach(function(el) { el.style.opacity = '0'; el.style.transform = 'translateY(24px)'; el.style.transition = 'opacity .5s ease, transform .5s ease'; obs.observe(el); }); } // ════════════════════════════════════════════════════════════════════ // SUPPORT FORM // ════════════════════════════════════════════════════════════════════ function initSupportForm() { var form = g('support-form'); var banner = g('success-banner'); if (!form || !banner) return; form.addEventListener('submit', function(e) { e.preventDefault(); banner.classList.add('visible'); form.reset(); setTimeout(function() { banner.classList.remove('visible'); }, 5000); }); } // ════════════════════════════════════════════════════════════════════ // BOOT — runs after DOM is ready // ════════════════════════════════════════════════════════════════════ document.addEventListener('DOMContentLoaded', function() { // Particles initParticles(); // Header scroll shadow var header = document.querySelector('.site-header'); if (header) { window.addEventListener('scroll', function() { header.classList.toggle('scrolled', window.scrollY > 40); }, { passive: true }); } // Footer year document.querySelectorAll('.year').forEach(function(el) { el.textContent = new Date().getFullYear(); }); // Scroll-reveal animations initReveal(); // Support form initSupportForm(); // iOS App nav button var navCta = g('nav-cta'); if (navCta) { navCta.addEventListener('click', function(e) { e.preventDefault(); var orig = navCta.textContent; navCta.textContent = '🔜 Coming Soon'; navCta.style.opacity = '.75'; setTimeout(function() { navCta.textContent = orig; navCta.style.opacity = ''; }, 2500); }); } // ── OCR Tool — only present on index.html ────────────────────── var uploadZone = g('upload-zone'); if (!uploadZone) return; // not on index page // Default engine activateTab('tesseract'); // ── Engine tab clicks ───────────────────────────────────────── // Use mousedown instead of click to fire before any focus events var tTess = g('tab-tesseract'); var tCloud = g('tab-ocrcloud'); if (tTess) { tTess.addEventListener('click', function(e) { e.stopPropagation(); activateTab('tesseract'); }); } if (tCloud) { tCloud.addEventListener('click', function(e) { e.stopPropagation(); activateTab('ocrcloud'); }); } // ── File input (zone is