(function() { 'use strict'; const video=document.getElementById('cameraVideo'), canvas=document.getElementById('trackingOverlay'); const ctx=canvas.getContext('2d'), label=document.getElementById('trackingEstado'); const R=window.Rastreamento; const fresh=new R.FreshTarget(); const connections=[[0,1],[1,2],[2,3],[3,4],[0,5],[5,6],[6,7],[7,8],[5,9],[9,10],[10,11],[11,12],[9,13],[13,14],[14,15],[15,16],[13,17],[0,17],[17,18],[18,19],[19,20]]; let worker=null, ready=false, pending=false, generation=0, active=false, config={}, session=null; let result=null, target=null, at=0, interval=50, nextFrame=0, lastVideo=-1, samples=0, retryAt=0, previousSample=0; const scratch=document.createElement('canvas'), sc=scratch.getContext('2d'); function clear() { ctx.clearRect(0,0,canvas.width,canvas.height); } function stop(text) { if (active && window.CarinhaVideo) window.CarinhaVideo.rastrear(null); active=false; generation++; if(worker) worker.terminate(); worker=null; pending=false; ready=false; result=null; target=null; lastVideo=-1; fresh.clear(); previousSample=0; clear(); label.textContent=text; } function draw() { const rect=canvas.parentElement.getBoundingClientRect(); if(canvas.width!==Math.round(rect.width) || canvas.height!==Math.round(rect.height)) { canvas.width=Math.round(rect.width); canvas.height=Math.round(rect.height); } clear(); canvas.hidden=!config.mostrarRastreamento; if(canvas.hidden || !result || performance.now()-at>350 || !video.videoWidth) return; const fit=R.fit(canvas.width,canvas.height,video.videoWidth,video.videoHeight); const point=p=>R.point(p,fit); ctx.lineWidth=2; ctx.font='13px Segoe UI'; for(const f of result.faces) { const p=point(f); ctx.strokeStyle='#5cd6ff'; ctx.fillStyle='#5cd6ff'; ctx.strokeRect(p.x,p.y,f.w*fit.w,f.h*fit.h); ctx.fillText('Rosto '+Math.round(f.score*100)+'%',p.x,Math.max(15,p.y-6)); for(const q of f.pontos || []) { const v=point(q); ctx.beginPath();ctx.arc(v.x,v.y,3,0,Math.PI*2);ctx.fill(); } } for(const h of result.hands) { ctx.strokeStyle='#78ffa0'; ctx.fillStyle='#78ffa0'; for(const [a,b] of connections) { const p=point(h[a]),q=point(h[b]); ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(q.x,q.y);ctx.stroke(); } for(const q of h) { const p=point(q);ctx.beginPath();ctx.arc(p.x,p.y,3,0,Math.PI*2);ctx.fill(); } const p=point(h[0]);ctx.fillText('Mão',p.x+6,p.y+16); } if(target) { const p=point(target);ctx.strokeStyle='#ffd860';ctx.beginPath();ctx.arc(p.x,p.y,12,0,Math.PI*2);ctx.moveTo(p.x-18,p.y);ctx.lineTo(p.x+18,p.y);ctx.moveTo(p.x,p.y-18);ctx.lineTo(p.x,p.y+18);ctx.stroke(); } } function start() { active=true; const gen=++generation; worker=new Worker('/rastreador-worker.js'); label.textContent='Carregando rastreadores locais…'; worker.onerror=e=>{ retryAt=Date.now()+10000; stop('Falha no rastreador: '+e.message); }; worker.onmessage=({data})=>{ if(gen!==generation) return; pending=false; if(data.tipo==='erro') { retryAt=Date.now()+10000; stop('Rastreamento indisponível: '+data.erro); return; } ready=true; if(data.tipo==='pronto') { label.textContent='Rastreadores prontos. Aguardando imagem…'; return; } result=data; at=data.timestamp; samples++; const prior=target; target=R.target(data.faces,data.hands,prior); if(target) target.atividade=!!prior && target.tipo===prior.tipo && Math.hypot(target.x-prior.x,target.y-prior.y)>.008; if(target) fresh.accept(target,at); else fresh.clear(); interval=Math.max(50,Math.min(200,data.ms*1.2)); const hz=previousSample?Math.round(1000/(data.timestamp-previousSample)):0; previousSample=data.timestamp; label.textContent=data.faces.length+' rosto(s) · '+data.hands.length+' mão(s) · '+hz+' análises/s · '+Math.round(data.ms)+' ms' + (target?' · alvo: '+(target.tipo==='mao'?'mão':'rosto'):' · procurando…'); if(config.seguirRastreamento && window.CarinhaVideo) window.CarinhaVideo.rastrear(target); draw(); }; pending=true; worker.postMessage({config:{rosto:config.rastrearRosto,maos:config.rastrearMaos}}); } async function frame(now) { if(!active || !ready || pending || now{const now=performance.now();frame(now); if(fresh.value && !fresh.get(now)) { fresh.clear(); target=null; if(config.seguirRastreamento && window.CarinhaVideo) window.CarinhaVideo.rastrear(null); } if(result && now-at>350) { result=null;target=null;clear(); } },20); window.addEventListener('resize',draw); window.CarinhaRastreamento={ atualizar(status,c) { const changed=config.rastrearRosto!==c.rastrearRosto || config.rastrearMaos!==c.rastrearMaos; if(config.seguirRastreamento && !c.seguirRastreamento && window.CarinhaVideo) window.CarinhaVideo.rastrear(null); config=c; const allowed=status.online && c.preview && c.sensorCamera!==false && (!(status.estado || {}).dormindo || c.cameraSono); if(!allowed || (!c.rastrearRosto && !c.rastrearMaos)) { stop(!allowed?'Ligue o vídeo para rastrear no PC.':'Rastreamento desligado.'); return; } if(changed && active) stop('Atualizando rastreadores…'); if(!active && Date.now()>=retryAt) start(); draw(); }, parar() { stop('Vídeo desligado.'); }, estado() { return {active,ready,pending,samples,result,target}; } }; window.addEventListener('pagehide',()=>stop('Painel fechado.')); })();