| <!DOCTYPE html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>PanelZoom</title> |
| <style> |
| :root{--bg:#0a0a0f;--sur:#13131a;--bor:#1e1e2e;--acc:#7c6aff;--acc2:#ff6b6b;--txt:#e8e6f0;--mut:#6b6880;} |
| *{box-sizing:border-box;margin:0;padding:0;} |
| body{background:var(--bg);color:var(--txt);font-family:'Segoe UI',system-ui,sans-serif; |
| height:100dvh;display:flex;flex-direction:column;overflow:hidden;user-select:none;-webkit-user-select:none;} |
| |
| |
| #up{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;gap:20px;} |
| .logo-icon{width:72px;height:72px;background:linear-gradient(135deg,var(--acc),var(--acc2)); |
| border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:36px; |
| box-shadow:0 8px 28px rgba(124,106,255,.3);} |
| h1{font-size:28px;font-weight:800;letter-spacing:-1px; |
| background:linear-gradient(135deg,var(--acc),var(--acc2)); |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent;} |
| .subtitle{color:var(--mut);font-size:13px;text-align:center;} |
| |
| #space-row{width:100%;max-width:420px;display:flex;flex-direction:column;gap:6px;} |
| #space-row label{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);font-weight:700;} |
| #space-input{background:var(--sur);border:1px solid var(--bor);border-radius:10px; |
| padding:11px 13px;color:var(--txt);font-size:13px;outline:none;width:100%;font-family:monospace;} |
| #space-input:focus{border-color:var(--acc);} |
| #space-hint{font-size:11px;color:var(--mut);} |
| |
| .drop{width:100%;max-width:420px;border:2px dashed var(--bor);border-radius:16px; |
| padding:38px 20px;text-align:center;cursor:pointer;transition:all .2s;background:var(--sur); |
| display:flex;flex-direction:column;align-items:center;gap:10px;} |
| .drop:hover,.drop.over{border-color:var(--acc);background:rgba(124,106,255,.06);} |
| .drop-icon{font-size:40px;opacity:.8;} |
| .drop h3{font-size:16px;font-weight:700;} |
| .drop p{font-size:12px;color:var(--mut);} |
| .badges{display:flex;gap:5px;flex-wrap:wrap;justify-content:center;} |
| .badge{background:var(--bor);border-radius:5px;padding:2px 8px;font-size:11px; |
| font-weight:700;color:var(--mut);font-family:monospace;} |
| #fi{display:none;} |
| |
| |
| #rd{display:none;flex:1;flex-direction:column;overflow:hidden;} |
| #topbar{display:flex;align-items:center;justify-content:space-between; |
| padding:8px 11px;background:var(--sur);border-bottom:1px solid var(--bor);gap:7px;min-height:48px;} |
| .tl{display:flex;align-items:center;gap:7px;min-width:0;} |
| .tr{display:flex;align-items:center;gap:5px;flex-shrink:0;} |
| .bi{width:33px;height:33px;border:none;background:var(--bor);color:var(--txt); |
| border-radius:8px;cursor:pointer;font-size:15px;display:flex;align-items:center; |
| justify-content:center;flex-shrink:0;transition:background .15s;} |
| .bi:hover{background:#2a2a3e;} |
| .bi:active{transform:scale(.92);} |
| #btitle{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden; |
| text-overflow:ellipsis;max-width:140px;} |
| #pinfo{font-size:12px;color:var(--mut);white-space:nowrap;} |
| .tg{display:flex;background:var(--bor);border-radius:7px;padding:2px;gap:2px;} |
| .tb{border:none;background:transparent;color:var(--mut);padding:3px 8px; |
| border-radius:5px;cursor:pointer;font-size:11px;font-weight:700;transition:all .15s;} |
| .tb.on{background:var(--acc);color:#fff;} |
| |
| #ca{flex:1;position:relative;overflow:hidden;touch-action:none;background:#050508;} |
| #pc{position:absolute;transform-origin:top left;} |
| #dc{position:absolute;top:0;left:0;transform-origin:top left;pointer-events:none;display:none;} |
| #hl{position:absolute;border:2.5px solid var(--acc);border-radius:9px; |
| box-shadow:0 0 0 9999px rgba(0,0,0,.58);pointer-events:none; |
| transition:all .38s cubic-bezier(.25,.46,.45,.94);opacity:0;z-index:10;} |
| |
| #strip{display:flex;gap:6px;padding:7px 11px;overflow-x:auto;background:#16161f; |
| border-top:1px solid var(--bor);min-height:66px;max-height:84px; |
| scrollbar-width:thin;scrollbar-color:var(--bor) transparent;} |
| .th{width:46px;height:62px;flex-shrink:0;border-radius:6px;overflow:hidden; |
| border:2px solid transparent;cursor:pointer;transition:border-color .2s;position:relative;} |
| .th.on{border-color:var(--acc);} |
| .th img{width:100%;height:100%;object-fit:cover;display:block;} |
| .thn{position:absolute;bottom:2px;right:2px;font-size:9px; |
| background:rgba(0,0,0,.65);padding:1px 3px;border-radius:3px;color:#fff;} |
| |
| #bb{display:flex;align-items:center;justify-content:space-between; |
| padding:8px 11px;background:var(--sur);border-top:1px solid var(--bor);gap:8px;min-height:56px;} |
| .nb{width:43px;height:43px;border:none;background:var(--bor);color:var(--txt); |
| border-radius:9px;cursor:pointer;font-size:22px;display:flex;align-items:center; |
| justify-content:center;transition:all .15s;flex-shrink:0;} |
| .nb:hover{background:#2a2a3e;} |
| .nb:active{transform:scale(.9);} |
| .nb:disabled{opacity:.25;cursor:default;transform:none;} |
| #bprog{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;min-width:0;} |
| #bcnt{font-size:12px;color:var(--mut);text-align:center;} |
| #bdots{display:flex;gap:5px;flex-wrap:wrap;justify-content:center;max-width:200px;} |
| .dot{width:7px;height:7px;border-radius:50%;background:var(--bor); |
| transition:all .2s;cursor:pointer;} |
| .dot.v{background:var(--mut);} |
| .dot.c{background:var(--acc);transform:scale(1.5);} |
| #dbtn{background:linear-gradient(135deg,var(--acc),#5a4fcf);border:none;color:#fff; |
| padding:8px 13px;border-radius:8px;font-size:12px;font-weight:700; |
| cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .15s;} |
| #dbtn:hover{opacity:.84;} |
| #dbtn:disabled{opacity:.35;cursor:default;} |
| |
| #so{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center; |
| justify-content:center;background:rgba(10,10,15,.88);z-index:50;gap:12px; |
| pointer-events:none;opacity:0;transition:opacity .25s;} |
| #so.on{opacity:1;pointer-events:all;} |
| .sp{width:36px;height:36px;border:3px solid var(--bor);border-top-color:var(--acc); |
| border-radius:50%;animation:spin .75s linear infinite;} |
| @keyframes spin{to{transform:rotate(360deg);}} |
| #stxt{font-size:13px;color:var(--mut);text-align:center;max-width:220px;} |
| #spb{width:160px;height:4px;background:var(--bor);border-radius:2px;overflow:hidden;} |
| #sbar{height:100%;background:var(--acc);border-radius:2px;transition:width .3s;width:0%;} |
| |
| #toast{position:fixed;bottom:68px;left:50%; |
| transform:translateX(-50%) translateY(14px); |
| background:#1e1e2e;border:1px solid var(--bor);color:var(--txt); |
| padding:9px 16px;border-radius:9px;font-size:13px;font-weight:500; |
| opacity:0;pointer-events:none;transition:all .28s; |
| z-index:200;max-width:290px;text-align:center;} |
| #toast.on{opacity:1;transform:translateX(-50%) translateY(0);} |
| #toast.err{background:rgba(255,80,80,.12);border-color:var(--acc2);} |
| |
| ::-webkit-scrollbar{width:4px;height:4px;} |
| ::-webkit-scrollbar-thumb{background:var(--bor);border-radius:4px;} |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div id="up"> |
| <div style="display:flex;flex-direction:column;align-items:center;gap:8px"> |
| <div class="logo-icon">π</div> |
| <h1>PanelZoom</h1> |
| <p class="subtitle">DetecciΓ³n de globos con YOLOv8 Β· Gratis Β· Sin instalar nada</p> |
| </div> |
|
|
| <div id="space-row"> |
| <label>Tu Hugging Face Space (usuario/nombre-del-space)</label> |
| <input id="space-input" type="text" placeholder="tunombre/panelzoom" spellcheck="false"> |
| <p id="space-hint">Ej: <code>maria/panelzoom-detector</code> β lo configurΓ‘s una vez y queda guardado</p> |
| </div> |
|
|
| <div class="drop" id="drop"> |
| <div class="drop-icon">π</div> |
| <h3>Abrir comic o manga</h3> |
| <p>ArrastrΓ‘ o tocΓ‘ para seleccionar</p> |
| <div class="badges"> |
| <span class="badge">CBZ</span> |
| <span class="badge">ZIP</span> |
| <span class="badge">PDF</span> |
| </div> |
| </div> |
| <input type="file" id="fi" accept=".cbz,.zip,.pdf"> |
| </div> |
|
|
| |
| <div id="rd"> |
| <div id="topbar"> |
| <div class="tl"> |
| <button class="bi" id="backbtn">β</button> |
| <span id="btitle">Comic</span> |
| </div> |
| <div class="tr"> |
| <span id="pinfo">1/1</span> |
| <div class="tg"> |
| <button class="tb on" id="mc">Comic</button> |
| <button class="tb" id="mm">Manga</button> |
| </div> |
| <button class="bi" id="debugbtn" title="Ver detecciones">π¬</button> |
| </div> |
| </div> |
|
|
| <div id="ca"> |
| <canvas id="pc"></canvas> |
| <canvas id="dc"></canvas> |
| <div id="hl"></div> |
| <div id="so"> |
| <div class="sp"></div> |
| <div id="stxt">Cargandoβ¦</div> |
| <div id="spb"><div id="sbar"></div></div> |
| </div> |
| </div> |
|
|
| <div id="strip"></div> |
|
|
| <div id="bb"> |
| <button class="nb" id="prevb">βΉ</button> |
| <div id="bprog"> |
| <div id="bcnt">TocΓ‘ π para detectar globos</div> |
| <div id="bdots"></div> |
| </div> |
| <button class="nb" id="nextb">βΊ</button> |
| <button id="dbtn">π Detectar</button> |
| </div> |
| </div> |
|
|
| <div id="toast"></div> |
|
|
| <script type="module"> |
| import * as pdfjsLib from 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.min.mjs'; |
| pdfjsLib.GlobalWorkerOptions.workerSrc = |
| 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.mjs'; |
| |
| |
| let JSZip = null; |
| async function getZip() { |
| if (JSZip) return JSZip; |
| for (const u of [ |
| 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js', |
| 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js', |
| ]) { |
| try { |
| await new Promise((r,j)=>{ const s=document.createElement('script'); |
| s.src=u;s.onload=r;s.onerror=j;document.head.appendChild(s); }); |
| if(window.JSZip){JSZip=window.JSZip;return JSZip;} |
| } catch(_){} |
| } |
| throw new Error('No se pudo cargar JSZip'); |
| } |
| |
| |
| const S = { |
| pages:[], currentPage:0, bubbles:{}, currentBubble:-1, |
| mangaMode:false, zoom:1, panX:0, panY:0, |
| zoomMargin:28, animSpeed:360, spaceUrl:'', |
| }; |
| |
| |
| const $=id=>document.getElementById(id); |
| const up=$('up'),rd=$('rd'),drop=$('drop'),fi=$('fi'); |
| const pc=$('pc'),dc=$('dc'),hl=$('hl'),ca=$('ca'),so=$('so'); |
| const ctx=pc.getContext('2d',{willReadFrequently:true}); |
| const dctx=dc.getContext('2d'); |
| const strip=$('strip'),bcnt=$('bcnt'),bdots=$('bdots'); |
| const stxt=$('stxt'),sbar=$('sbar'),toast=$('toast'); |
| const dbtn=$('dbtn'),spaceInput=$('space-input'); |
| |
| |
| const STORAGE_KEY = 'panelzoom_space'; |
| try { spaceInput.value = localStorage.getItem(STORAGE_KEY) || ''; } catch(_){} |
| spaceInput.addEventListener('blur', () => { |
| try { localStorage.setItem(STORAGE_KEY, spaceInput.value.trim()); } catch(_){} |
| }); |
| |
| function getApiUrl() { |
| const val = spaceInput.value.trim(); |
| if (!val) return null; |
| |
| if (val.startsWith('http')) return val; |
| return `https://${val.replace('/','-')}.hf.space/call/detect`; |
| } |
| |
| |
| let toastT; |
| function toast_(msg, type='', dur=3000) { |
| clearTimeout(toastT); |
| toast.textContent=msg; toast.className='on'+(type?' '+type:''); |
| toastT=setTimeout(()=>toast.className='',dur); |
| } |
| |
| |
| function showS(t,p=-1){stxt.textContent=t;sbar.style.width=p>=0?p+'%':'0%';so.classList.add('on');} |
| function hideS(){so.classList.remove('on');} |
| function setProg(p){sbar.style.width=p+'%';} |
| |
| |
| drop.addEventListener('click',()=>fi.click()); |
| drop.addEventListener('dragover',e=>{e.preventDefault();drop.classList.add('over');}); |
| drop.addEventListener('dragleave',()=>drop.classList.remove('over')); |
| drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('over'); |
| if(e.dataTransfer.files[0])openFile(e.dataTransfer.files[0]);}); |
| fi.addEventListener('change',()=>{ if(fi.files[0])openFile(fi.files[0]); }); |
| |
| async function openFile(file) { |
| up.style.display='none'; rd.style.display='flex'; |
| showS('Cargandoβ¦',0); |
| $('btitle').textContent = file.name.replace(/\.[^.]+$/,''); |
| S.pages=[];S.bubbles={};S.currentPage=0;S.currentBubble=-1; |
| strip.innerHTML=''; |
| |
| try { |
| if (file.name.toLowerCase().endsWith('.pdf')) await loadPDF(file); |
| else await loadCBZ(file); |
| buildStrip(); |
| await renderPage(0); |
| hideS(); |
| toast_(`β ${S.pages.length} pΓ‘ginas. TocΓ‘ π Detectar.`); |
| } catch(e) { |
| console.error(e); |
| toast_('Error: '+e.message,'err',5000); |
| goBack(); |
| } |
| } |
| |
| async function loadPDF(file) { |
| const pdf = await pdfjsLib.getDocument({data: await file.arrayBuffer()}).promise; |
| const n = pdf.numPages; |
| for (let i=1;i<=n;i++) { |
| showS(`Renderizando ${i}/${n}β¦`,(i/n)*100); |
| const page = await pdf.getPage(i); |
| const vp = page.getViewport({scale:2}); |
| const c = document.createElement('canvas'); |
| c.width=vp.width; c.height=vp.height; |
| await page.render({canvasContext:c.getContext('2d',{willReadFrequently:true}),viewport:vp}).promise; |
| S.pages.push({url:c.toDataURL('image/jpeg',.88),width:vp.width,height:vp.height}); |
| } |
| } |
| |
| async function loadCBZ(file) { |
| const Z = await getZip(); |
| const zip = await Z.loadAsync(await file.arrayBuffer()); |
| const imgRx = /\.(jpe?g|png|webp|gif|bmp)$/i; |
| const files = Object.values(zip.files) |
| .filter(f=>!f.dir&&imgRx.test(f.name)) |
| .sort((a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true})); |
| if(!files.length) throw new Error('No hay imΓ‘genes en el archivo.'); |
| const n=files.length; |
| for(let i=0;i<n;i++){ |
| showS(`Extrayendo ${i+1}/${n}β¦`,(i/n)*100); |
| const blob=await files[i].async('blob'); |
| const url=URL.createObjectURL(blob); |
| const {w,h}=await imgDims(url); |
| S.pages.push({url,width:w,height:h}); |
| } |
| } |
| |
| function imgDims(url){ |
| return new Promise((res,rej)=>{ |
| const img=new Image(); |
| img.onload=()=>res({w:img.naturalWidth,h:img.naturalHeight}); |
| img.onerror=rej; img.src=url; |
| }); |
| } |
| |
| |
| function buildStrip(){ |
| strip.innerHTML=''; |
| S.pages.forEach((p,i)=>{ |
| const t=document.createElement('div'); |
| t.className='th'+(i===0?' on':''); |
| t.innerHTML=`<img src="${p.url}" loading="lazy"><span class="thn">${i+1}</span>`; |
| t.addEventListener('click',()=>goToPage(i)); |
| strip.appendChild(t); |
| }); |
| } |
| function updateStrip(){ |
| document.querySelectorAll('.th').forEach((t,i)=>t.classList.toggle('on',i===S.currentPage)); |
| const el=strip.children[S.currentPage]; |
| if(el)el.scrollIntoView({behavior:'smooth',block:'nearest',inline:'center'}); |
| } |
| |
| |
| async function renderPage(i){ |
| if(i<0||i>=S.pages.length)return; |
| S.currentPage=i; S.currentBubble=-1; |
| await new Promise(res=>{ |
| const img=new Image(); |
| img.onload=()=>{ |
| pc.width=img.naturalWidth; pc.height=img.naturalHeight; |
| dc.width=pc.width; dc.height=pc.height; |
| ctx.drawImage(img,0,0); |
| if(S.debugMode)drawDebug();else dctx.clearRect(0,0,dc.width,dc.height); |
| fitPage(); |
| $('pinfo').textContent=`${i+1}/${S.pages.length}`; |
| updateStrip(); updateBubbleUI(); res(); |
| }; |
| img.src=S.pages[i].url; |
| }); |
| } |
| |
| function fitPage(){ |
| const aw=ca.clientWidth,ah=ca.clientHeight; |
| const s=Math.min(aw/pc.width,ah/pc.height); |
| S.zoom=s; S.panX=(aw-pc.width*s)/2; S.panY=(ah-pc.height*s)/2; |
| applyT(false); hl.style.opacity='0'; |
| } |
| function applyT(anim=true){ |
| const t=anim?`transform ${S.animSpeed}ms cubic-bezier(.25,.46,.45,.94)`:'none'; |
| [pc,dc].forEach(el=>{ |
| el.style.transition=t; |
| el.style.transform=`translate(${S.panX}px,${S.panY}px) scale(${S.zoom})`; |
| el.style.left='0';el.style.top='0'; |
| }); |
| } |
| function goToPage(i){renderPage(i);} |
| function nextPage(){if(S.currentPage<S.pages.length-1)renderPage(S.currentPage+1);} |
| function prevPage(){if(S.currentPage>0)renderPage(S.currentPage-1);} |
| |
| |
| function updateBubbleUI(){ |
| const bs=S.bubbles[S.currentPage]; |
| bdots.innerHTML=''; |
| if(!bs||!bs.length){ |
| bcnt.textContent='TocΓ‘ π Detectar para analizar la pΓ‘gina'; |
| return; |
| } |
| bcnt.textContent=S.currentBubble===-1 |
| ?`${bs.length} globo${bs.length!==1?'s':''} detectado${bs.length!==1?'s':''}` |
| :`Globo ${S.currentBubble+1} de ${bs.length}`; |
| bs.forEach((_,i)=>{ |
| const d=document.createElement('div'); |
| d.className='dot'+(i<S.currentBubble?' v':'')+(i===S.currentBubble?' c':''); |
| d.addEventListener('click',()=>goToBubble(i)); |
| bdots.appendChild(d); |
| }); |
| } |
| |
| function goToBubble(i){ |
| const bs=S.bubbles[S.currentPage]; |
| if(!bs||!bs[i])return; |
| S.currentBubble=i; zoomTo(bs[i]); updateBubbleUI(); |
| } |
| |
| function zoomTo(b){ |
| const aw=ca.clientWidth,ah=ca.clientHeight,m=S.zoomMargin; |
| const bx=b.x*pc.width,by=b.y*pc.height,bw=b.w*pc.width,bh=b.h*pc.height; |
| const nz=Math.min((aw-m*2)/bw,(ah-m*2)/bh,5); |
| S.zoom=nz; S.panX=aw/2-(bx+bw/2)*nz; S.panY=ah/2-(by+bh/2)*nz; |
| applyT(true); |
| setTimeout(()=>{ |
| hl.style.transition=`all ${S.animSpeed}ms cubic-bezier(.25,.46,.45,.94)`; |
| hl.style.left=(S.panX+bx*nz)+'px'; hl.style.top=(S.panY+by*nz)+'px'; |
| hl.style.width=(bw*nz)+'px'; hl.style.height=(bh*nz)+'px'; |
| hl.style.opacity='1'; |
| },16); |
| } |
| |
| $('nextb').addEventListener('click',()=>{ |
| const bs=S.bubbles[S.currentPage]; |
| if(!bs||!bs.length){nextPage();return;} |
| S.currentBubble<bs.length-1?goToBubble(S.currentBubble+1):nextPage(); |
| }); |
| $('prevb').addEventListener('click',()=>{ |
| const bs=S.bubbles[S.currentPage]; |
| if(!bs||!bs.length){prevPage();return;} |
| if(S.currentBubble>0)goToBubble(S.currentBubble-1); |
| else if(S.currentBubble===0){S.currentBubble=-1;fitPage();updateBubbleUI();} |
| else prevPage(); |
| }); |
| |
| |
| dbtn.addEventListener('click', detectPage); |
| |
| async function detectPage(){ |
| const apiUrl = getApiUrl(); |
| if (!apiUrl) { |
| toast_('IngresΓ‘ tu Space de Hugging Face arriba primero.','err'); |
| goBack(); return; |
| } |
| if (S.detecting) return; |
| S.detecting=true; dbtn.disabled=true; dbtn.textContent='β³ Detectandoβ¦'; |
| showS('Enviando pΓ‘gina al Spaceβ¦',15); |
| |
| try { |
| |
| const tmpC=document.createElement('canvas'); |
| tmpC.width=pc.width; tmpC.height=pc.height; |
| tmpC.getContext('2d').drawImage(pc,0,0); |
| const dataUrl=tmpC.toDataURL('image/jpeg',.82); |
| const b64=dataUrl.split(',')[1]; |
| |
| setProg(30); |
| |
| |
| const queueRes = await fetch(apiUrl, { |
| method:'POST', |
| headers:{'Content-Type':'application/json'}, |
| body: JSON.stringify({ |
| data: [b64, 0.30, 0.45, S.mangaMode] |
| }) |
| }); |
| |
| if (!queueRes.ok) throw new Error(`HTTP ${queueRes.status} β ΒΏEl Space estΓ‘ corriendo?`); |
| |
| const queueData = await queueRes.json(); |
| const eventId = queueData.event_id; |
| if (!eventId) throw new Error('El Space no devolviΓ³ event_id. VerificΓ‘ que estΓ© activo.'); |
| |
| setProg(50); |
| |
| |
| const resultUrl = apiUrl.replace('/call/detect', `/call/detect/${eventId}`); |
| let result = null; |
| for (let i=0; i<60; i++) { |
| await new Promise(r=>setTimeout(r,1200)); |
| setProg(50 + i); |
| const ev = await fetch(resultUrl); |
| const text = await ev.text(); |
| const lines = text.trim().split('\n'); |
| for (const line of lines) { |
| if (line.startsWith('data:')) { |
| const parsed = JSON.parse(line.slice(5)); |
| if (Array.isArray(parsed) && parsed[0]) { |
| result = JSON.parse(parsed[0]); |
| break; |
| } |
| } |
| } |
| if (result) break; |
| } |
| |
| if (!result) throw new Error('Timeout β el Space tardΓ³ demasiado.'); |
| if (!result.ok) throw new Error(result.error || 'Error desconocido en el Space.'); |
| |
| const bubbles = result.bubbles || []; |
| S.bubbles[S.currentPage] = bubbles; |
| S.currentBubble = -1; |
| if (S.debugMode) drawDebug(); |
| |
| if (!bubbles.length) toast_('No se detectaron globos en esta pΓ‘gina.'); |
| else { toast_(`β ${bubbles.length} globo${bubbles.length!==1?'s':''} detectado${bubbles.length!==1?'s':''}`); goToBubble(0); } |
| updateBubbleUI(); |
| |
| } catch(e) { |
| toast_('Error: '+e.message,'err',5000); |
| console.error(e); |
| } finally { |
| S.detecting=false; dbtn.disabled=false; dbtn.textContent='π Detectar'; |
| hideS(); |
| } |
| } |
| |
| |
| function drawDebug(){ |
| const bs=S.bubbles[S.currentPage]; |
| dctx.clearRect(0,0,dc.width,dc.height); |
| if(!bs||!bs.length)return; |
| bs.forEach((b,i)=>{ |
| const x=b.x*dc.width,y=b.y*dc.height,w=b.w*dc.width,h=b.h*dc.height; |
| dctx.strokeStyle=`hsl(${i*53%360},80%,60%)`; |
| dctx.lineWidth=3; dctx.strokeRect(x,y,w,h); |
| dctx.fillStyle=dctx.strokeStyle; |
| dctx.font='bold 14px sans-serif'; |
| dctx.fillText(`${i+1}`,x+4,y+16); |
| }); |
| } |
| $('debugbtn').addEventListener('click',()=>{ |
| S.debugMode=!S.debugMode; |
| dc.style.display=S.debugMode?'block':'none'; |
| $('debugbtn').style.background=S.debugMode?'var(--acc)':''; |
| if(S.debugMode)drawDebug();else dctx.clearRect(0,0,dc.width,dc.height); |
| }); |
| |
| |
| function setManga(m){ |
| S.mangaMode=m; |
| $('mc').classList.toggle('on',!m); $('mm').classList.toggle('on',m); |
| } |
| $('mc').addEventListener('click',()=>setManga(false)); |
| $('mm').addEventListener('click',()=>setManga(true)); |
| |
| |
| $('backbtn').addEventListener('click',goBack); |
| function goBack(){ |
| S.pages.forEach(p=>{try{URL.revokeObjectURL(p.url);}catch(_){}}); |
| S.pages=[];S.bubbles={}; |
| up.style.display='';rd.style.display='none'; |
| hl.style.opacity='0';fi.value=''; |
| } |
| |
| |
| document.addEventListener('keydown',e=>{ |
| if(up.style.display!=='none')return; |
| switch(e.key){ |
| case 'ArrowRight':case ' ':e.preventDefault();$('nextb').click();break; |
| case 'ArrowLeft':e.preventDefault();$('prevb').click();break; |
| case 'ArrowUp':prevPage();break; |
| case 'ArrowDown':nextPage();break; |
| case 'f':fitPage();break; |
| case 'Escape':S.currentBubble=-1;fitPage();updateBubbleUI();break; |
| } |
| }); |
| |
| |
| let tx=0,ty=0,tt=0; |
| ca.addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY;tt=Date.now();},{passive:true}); |
| ca.addEventListener('touchend',e=>{ |
| const dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty; |
| const dt=Date.now()-tt,d=Math.hypot(dx,dy); |
| if(dt<300&&d<12){$('nextb').click();return;} |
| if(dt<500&&Math.abs(dx)>55&&Math.abs(dx)>Math.abs(dy)) |
| dx<0?$('nextb').click():$('prevb').click(); |
| },{passive:true}); |
| |
| let lp=0; |
| ca.addEventListener('touchstart',e=>{if(e.touches.length===2)lp=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);},{passive:true}); |
| ca.addEventListener('touchmove',e=>{ |
| if(e.touches.length!==2)return; |
| const d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY); |
| const r=d/lp;lp=d; |
| const cx=(e.touches[0].clientX+e.touches[1].clientX)/2,cy=(e.touches[0].clientY+e.touches[1].clientY)/2; |
| S.zoom=Math.max(.1,Math.min(7,S.zoom*r)); |
| S.panX=cx-(cx-S.panX)*r;S.panY=cy-(cy-S.panY)*r; |
| applyT(false); |
| },{passive:true}); |
| |
| ca.addEventListener('wheel',e=>{ |
| e.preventDefault(); |
| const r=e.deltaY<0?1.13:1/1.13,nz=Math.max(.1,Math.min(7,S.zoom*r)); |
| const rect=ca.getBoundingClientRect(); |
| S.panX=(e.clientX-rect.left)-(e.clientX-rect.left-S.panX)*(nz/S.zoom); |
| S.panY=(e.clientY-rect.top)-(e.clientY-rect.top-S.panY)*(nz/S.zoom); |
| S.zoom=nz; applyT(false); |
| },{passive:false}); |
| |
| new ResizeObserver(()=>{if(S.pages.length&&S.currentBubble===-1)fitPage();}).observe(ca); |
| </script> |
| </body> |
| </html> |
|
|