/* ================================================================ FACE SHAPE SUNGLASSES ANALYZER — MALE (profreetools.online) v15 | NEW URLS + FLOOD-FILL KNOCKOUT + KO_CACHE DOWNLOAD + toBlob GALLERY ================================================================ */ (function(){ 'use strict'; var SUNGLASSES_URL = { Oval: 'https://profreetools.online/wp-content/uploads/2026/05/OVAL-—-Classic-Aviator1.webp', Round: 'https://profreetools.online/wp-content/uploads/2026/05/ROUND-—-Structured-SquareWayfarer.webp', Square: 'https://profreetools.online/wp-content/uploads/2026/05/SQUARE-—-Subtle-RoundOval-Frame.webp', Heart: 'https://profreetools.online/wp-content/uploads/2026/05/HEART-—-Semi-Rimless-Clubmaster-1.webp', Diamond: 'https://profreetools.online/wp-content/uploads/2026/05/DIAMOND-—-Bold-Square-Frame.webp', Oblong: 'https://profreetools.online/wp-content/uploads/2026/05/OBLONG-—-Oversized-ShieldRectangle.webp', Triangle:'https://profreetools.online/wp-content/uploads/2026/05/TRIANGLE-—-BrowlinePilot-Teardrop.webp' }; var SHAPE_ORDER = ['Oval','Round','Square','Heart','Diamond','Oblong','Triangle']; var SHAPE_SUB = {Oval:'Classic Aviator',Round:'Structured Square',Square:'Subtle Round/Oval',Heart:'Semi-Rimless',Diamond:'Bold Square',Oblong:'Oversized Shield',Triangle:'Browline Classic'}; var FRAME_TUNE = {Oval:{wMul:2.55,yOff:-0.05},Round:{wMul:2.58,yOff:-0.04},Square:{wMul:2.52,yOff:-0.05},Heart:{wMul:2.50,yOff:-0.06},Diamond:{wMul:2.55,yOff:-0.05},Oblong:{wMul:2.70,yOff:-0.04},Triangle:{wMul:2.55,yOff:-0.06}}; var SCIENCE = { Oval: 'Because your face shape is detected as Oval with naturally balanced proportions, our system recommends versatile Aviator frames that complement your symmetrical structure without overpowering it.', Round: 'Because your face shape is detected as Round with soft curves, our system recommends Square frames that introduce strong angular contrast to elongate and define your facial structure.', Square: 'Because your face shape is detected as Square with a strong jawline, our system recommends Round/Oval frames whose curved lines soften angular features for a balanced, refined look.', Heart: 'Because your face shape is detected as Heart with a wide forehead, our system recommends Semi-Rimless frames that draw attention downward and balance your prominent upper face.', Diamond: 'Because your face shape is detected as Diamond with prominent cheekbones, our system recommends Bold Square frames that widen the forehead and jaw for complete facial harmony.', Oblong: 'Because your face shape is detected as Oblong with greater length than width, our system recommends Oversized Shield frames that add horizontal visual weight and balance the elongated profile.', Triangle:'Because your face shape is detected as Triangle with a wider jaw, our system recommends Browline frames that emphasize the upper face and create a balanced triangular symmetry.' }; var state = {naturalW:0,naturalH:0,displayedW:0,displayedH:0,photoOffsetX:0,photoOffsetY:0,landmarks:null,detectedShape:'Oval',activeShape:'Oval',shapeScores:{},modelsReady:false,offsetX:0,offsetY:0,scale:1}; var KO_CACHE = {}; var camStream = null; var initialized = false; function $(id){ return document.getElementById(id); } /* ── SMART LOADER — fetch+blob bypass CORS, flood-fill knockout ── */ function loadShapeAsset(shape){ return new Promise(function(resolve){ var url = SUNGLASSES_URL[shape]; if (!url){ resolve(''); return; } if (KO_CACHE[shape]){ resolve(KO_CACHE[shape]); return; } fetch(url + (url.indexOf('?')>-1?'&':'?') + 'ko=' + Date.now()) .then(function(r){ return r.blob(); }) .then(function(blob){ var blobUrl = URL.createObjectURL(blob); var img = new Image(); img.onload = function(){ try { var cv = document.createElement('canvas'); cv.width = img.naturalWidth; cv.height = img.naturalHeight; var ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0); var imageData = ctx.getImageData(0, 0, cv.width, cv.height); var bg = detectBG(imageData, cv.width, cv.height); knockoutBG(imageData, bg.r, bg.g, bg.b, cv.width, cv.height); ctx.putImageData(imageData, 0, 0); /* Auto-crop side temples — keep only tall front-frame region */ var cropped = cropTemples(cv, ctx); var dataUrl = cropped.toDataURL('image/png'); KO_CACHE[shape] = dataUrl; URL.revokeObjectURL(blobUrl); resolve(dataUrl); } catch(e){ URL.revokeObjectURL(blobUrl); KO_CACHE[shape]=url; resolve(url); } }; img.onerror = function(){ URL.revokeObjectURL(blobUrl); KO_CACHE[shape]=url; resolve(url); }; img.src = blobUrl; }) .catch(function(){ var img2 = new Image(); img2.crossOrigin = 'anonymous'; img2.onload = function(){ try { var cv2 = document.createElement('canvas'); cv2.width = img2.naturalWidth; cv2.height = img2.naturalHeight; var ctx2 = cv2.getContext('2d'); ctx2.drawImage(img2, 0, 0); var id2 = ctx2.getImageData(0, 0, cv2.width, cv2.height); var bg2 = detectBG(id2, cv2.width, cv2.height); knockoutBG(id2, bg2.r, bg2.g, bg2.b, cv2.width, cv2.height); ctx2.putImageData(id2, 0, 0); var cropped2 = cropTemples(cv2, ctx2); KO_CACHE[shape] = cropped2.toDataURL('image/png'); resolve(KO_CACHE[shape]); } catch(e2){ KO_CACHE[shape]=url; resolve(url); } }; img2.onerror = function(){ KO_CACHE[shape]=url; resolve(url); }; img2.src = url; }); }); } /* ── AUTO TEMPLE CROP ── After knockout, scan each column's opaque-pixel height. Lens/frame area = tall columns. Temples = short thin columns. Crop left/right to the tall front-frame region only. */ function cropTemples(cv, ctx){ var W=cv.width, H=cv.height; var data=ctx.getImageData(0,0,W,H).data; /* Measure opaque height per column */ var colH=new Array(W).fill(0); var maxH=0; for(var x=0;x40){ if(top<0)top=y; bot=y; } } var h=(top<0)?0:(bot-top+1); colH[x]=h; if(h>maxH)maxH=h; } if(maxH<10) return cv; /* nothing to crop */ /* Front frame = columns where height >= 55% of max (lenses are tall). Temples are thin (<45% height) — exclude them. */ var thresh=maxH*0.55; var left=-1, right=-1; for(var cx=0;cx=thresh){ left=cx; break; } } for(var cx2=W-1;cx2>=0;cx2--){ if(colH[cx2]>=thresh){ right=cx2; break; } } if(left<0||right<0||right<=left) return cv; /* Small padding so frame edges aren't clipped */ var pad=Math.round((right-left)*0.04); left=Math.max(0,left-pad); right=Math.min(W-1,right+pad); /* Vertical bounds within the kept region */ var top2=H, bot2=0; for(var vx=left;vx<=right;vx++){ for(var vy=0;vy40){ if(vybot2)bot2=vy; } } } var vpad=Math.round((bot2-top2)*0.05); top2=Math.max(0,top2-vpad); bot2=Math.min(H-1,bot2+vpad); var cw=right-left+1, ch=bot2-top2+1; if(cw<10||ch<10) return cv; /* Crop to new canvas */ var out=document.createElement('canvas'); out.width=cw; out.height=ch; var octx=out.getContext('2d'); octx.drawImage(cv, left, top2, cw, ch, 0, 0, cw, ch); return out; } function detectBG(d, W, H){ var data=d.data, pts=[[2,2],[W-3,2],[2,H-3],[W-3,H-3],[Math.floor(W/4),2],[Math.floor(W/2),2],[Math.floor(3*W/4),2],[Math.floor(W/4),H-3],[Math.floor(W/2),H-3],[Math.floor(3*W/4),H-3],[2,Math.floor(H/2)],[W-3,Math.floor(H/2)]]; var rs=[],gs=[],bs=[]; for(var i=0;i230 && r>215 && g>215 && b>215); } var head=0; while(head0)queue.push(idx-1); if(px0)queue.push(idx-W); if(py0&&data[(fi-1)*4+3]===0)tn++; if(fx0&&data[(fi-W)*4+3]===0)tn++; if(fy0)data[fi*4+3]=Math.round(data[fi*4+3]*(1-tn*0.18)); } } /* ── PILLS ── */ function renderPills(){ var track = $('fssmShapeTrack'); if (!track) return; var html = ''; for(var i=0;i'+s+''+SHAPE_SUB[s]+''; } track.innerHTML = html; var pills = track.querySelectorAll('.fssm-shape-pill'); for(var j=0;j
'+s+'0.0%
';} el.innerHTML=html; } function updateBars(scores){ var el=$('fssmBars'); if(!el)return; var rows=el.querySelectorAll('.fssm-bar-row'); for(var i=0;i0?(match/top):1)); var sym=Math.max(0.62,Math.min(0.96,0.70+match*0.30)),prop=Math.max(0.60,Math.min(0.93,0.66+align*0.30)),ov=Math.max(0.55,Math.min(0.94,sym*0.45+prop*0.35+match*0.20)); animateGauge('fssmG1','fssmG1V',sym);animateGauge('fssmG2','fssmG2V',prop);animateGauge('fssmG3','fssmG3V',ov); } /* ── FACE API ── */ var MODEL_URL='https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/weights'; var modelLoadPromise=null; function loadModels(){ if(modelLoadPromise)return modelLoadPromise; modelLoadPromise=new Promise(function(resolve){ if(typeof faceapi!=='undefined'){startML(resolve);return;} var s=document.createElement('script'); s.src='https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js'; s.async=true;s.setAttribute('data-fssm','1'); s.onload=function(){startML(resolve);};s.onerror=function(){resolve(false);}; document.head.appendChild(s); }); return modelLoadPromise; } function startML(resolve){ try{Promise.all([faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)]).then(function(){state.modelsReady=true;resolve(true);}).catch(function(){resolve(false);});}catch(e){resolve(false);} } function detectFace(){ return new Promise(function(resolve){ var photo=$('fssmPhoto'); if(!state.modelsReady||typeof faceapi==='undefined'||!photo){resolve(null);return;} try{ faceapi.detectSingleFace(photo,new faceapi.TinyFaceDetectorOptions({inputSize:320,scoreThreshold:0.45})).withFaceLandmarks().then(function(res){ if(!res){resolve(null);return;} try{ var lm=res.landmarks,le=centroid(lm.getLeftEye()),re=centroid(lm.getRightEye()); var dx=re.x-le.x,dy=re.y-le.y,ipd=Math.sqrt(dx*dx+dy*dy),ang=Math.atan2(dy,dx)*180/Math.PI; var nosePts=lm.getNose(),noseTip=nosePts[nosePts.length-1],emX=(le.x+re.x)/2; var yaw=Math.max(-1,Math.min(1,(noseTip.x-emX)/(ipd*0.6))); resolve({leftEye:le,rightEye:re,ipdPx:ipd,angleDeg:ang,centerX:emX,centerY:(le.y+re.y)/2,yawNorm:yaw,allLandmarks:lm.positions,faceBox:res.detection.box}); }catch(e){resolve(null);} }).catch(function(){resolve(null);}); }catch(e){resolve(null);} }); } function centroid(pts){var x=0,y=0;for(var i=0;i1.02?0.15:0),Round:1-Math.abs(lw-1.05)*1.8+(ja>150?0.20:0),Square:1-Math.abs(lw-1.10)*1.6+(ja<135?0.30:0)+(Math.abs(fj-1)<0.10?0.15:0),Heart:1-Math.abs(lw-1.30)*1.5+(fj>1.12?0.30:0),Diamond:1-Math.abs(lw-1.35)*1.5+(cf>1.15?0.25:0),Oblong:1-Math.abs(lw-1.60)*1.4+(lw>1.50?0.30:0),Triangle:1-Math.abs(lw-1.30)*1.5+(fj<0.92?0.30:0)}; var sum=0;for(var k in raw){if(raw[k]<0.02)raw[k]=0.02;sum+=raw[k];} var out={};for(var k2 in raw){out[k2]=raw[k2]/sum;}return out; }catch(e){return{Oval:0.62,Round:0.10,Square:0.06,Heart:0.10,Diamond:0.06,Oblong:0.04,Triangle:0.02};} } function pickTop(scores){var best='Oval',val=-1;for(var k in scores){if(scores[k]>val){val=scores[k];best=k;}}return best;} /* ── MEASURE + PLACE ── */ function measureDisplay(){ var box=$('fssmImageBox'); if(!state.naturalW||!state.naturalH||!box)return; var r=box.getBoundingClientRect(),bw=r.width,bh=r.height; if(!bw||!bh)return; var ratio=state.naturalW/state.naturalH,dw,dh; if(bw/bh>ratio){dh=bh;dw=dh*ratio;}else{dw=bw;dh=dw/ratio;} state.displayedW=dw;state.displayedH=dh;state.photoOffsetX=(bw-dw)/2;state.photoOffsetY=(bh-dh)/2; } function placeGlasses(){ var g=$('fssmGlassesImg'); if(!g||!state.naturalW||!g.src||!state.landmarks)return; var lm=state.landmarks,sc=state.displayedW/state.naturalW; var cx=state.photoOffsetX+lm.centerX*sc,cy=state.photoOffsetY+lm.centerY*sc; var ipd=lm.ipdPx*sc,tune=FRAME_TUNE[state.activeShape]||{wMul:2.55,yOff:-0.05}; var fw=ipd*tune.wMul*state.scale,px=cx+state.offsetX,py=cy+ipd*tune.yOff+state.offsetY; var yaw=lm.yawNorm||0,sx=Math.max(0.78,1-Math.abs(yaw)*0.22); g.style.width=fw.toFixed(1)+'px';g.style.height='auto';g.style.left='0';g.style.top='0'; g.style.transform='translate3d('+(px-fw/2).toFixed(1)+'px,'+(py).toFixed(1)+'px,0) translateY(-50%) rotate3d(0,0,1,'+lm.angleDeg.toFixed(2)+'deg) rotate3d(0,1,0,'+(yaw*16).toFixed(2)+'deg) scaleX('+sx.toFixed(3)+')'; g.classList.add('ready'); } /* ── SCAN — v13 PERMANENT FIX ── Root cause of blue line staying: CSS animation was 'infinite' — even after display:none the animation continued in memory and could re-show. Fix: Stop the animation AND hide the overlay AND remove class. Triple-lock: all 3 must be done together. ── */ function showScan(on){ var s=$('fssmScanOverlay'); var laser=s?s.querySelector('.fssm-scan-laser'):null; if(!s)return; if(on){ /* Reset animation fresh each time */ if(laser){ laser.style.animationName='none'; /* Force reflow to restart animation */ void laser.offsetHeight; laser.style.animationName=''; } s.style.display='block'; s.classList.add('show'); } else { /* TRIPLE LOCK: stop animation + hide + remove class */ if(laser){ laser.style.animationName='none'; laser.style.animation='none'; } s.classList.remove('show'); s.style.display='none'; } } /* ── v12 ARCHITECTURE: scan stop INDEPENDENT of loadModels ── */ function onPhotoLoaded(){ var photo=$('fssmPhoto'); if(!photo)return; try{state.naturalW=photo.naturalWidth||photo.width;state.naturalH=photo.naturalHeight||photo.height;}catch(_){} requestAnimationFrame(function(){ measureDisplay(); showScan(true); /* GUARANTEED 1.5s stop — nothing can delay this */ setTimeout(function(){ showScan(false); var lm=fallbackLandmarks(); state.landmarks=lm; state.shapeScores=scoreShapes(lm); state.detectedShape=pickTop(state.shapeScores); var nm=$('fssmShapeName');if(nm)nm.textContent=state.detectedShape; updateBars(state.shapeScores); setActiveShape(state.detectedShape,false); computeMetrics(state.detectedShape); placeGlasses(); if(window.innerWidth<980){var h=$('fssmDragHint');if(h){h.classList.add('show');setTimeout(function(){h.classList.remove('show');},3000);}} /* Background AI — silently updates if fast */ loadModels().then(function(ok){ if(!ok)return; detectFace().then(function(lm2){ if(!lm2)return; state.landmarks=lm2;state.shapeScores=scoreShapes(lm2);state.detectedShape=pickTop(state.shapeScores); var nm2=$('fssmShapeName');if(nm2)nm2.textContent=state.detectedShape; updateBars(state.shapeScores);setActiveShape(state.detectedShape,false);computeMetrics(state.detectedShape);placeGlasses(); }); }); },1500); }); } function loadPhotoFromSrc(src){ var photo=$('fssmPhoto'); if(!photo)return; photo.onload=function(){ var hero=$('fssmHeroSplit'),stage=$('fssmStage'); if(hero)hero.classList.add('hide'); if(stage)stage.classList.add('show'); state.offsetX=0;state.offsetY=0;state.scale=1; requestAnimationFrame(function(){requestAnimationFrame(onPhotoLoaded);}); }; photo.onerror=function(){alert('Could not load image. Please try another photo.');}; photo.src=src; } /* ── UPLOAD ── */ function bindUpload(){ var upBtn=$('fssmUpBtn'),fileInput=$('fssmFile'); if(!upBtn||!fileInput)return; upBtn.addEventListener('click',function(e){e.preventDefault();fileInput.click();},false); upBtn.addEventListener('touchend',function(e){e.preventDefault();e.stopPropagation();fileInput.click();},false); fileInput.addEventListener('change',function(e){ var f=e.target.files&&e.target.files[0]; if(!f)return; if(!/^image\//.test(f.type)){alert('Please choose an image file.');return;} var reader=new FileReader(); reader.onload=function(ev){loadPhotoFromSrc(ev.target.result);}; reader.readAsDataURL(f); fileInput.value=''; },false); } /* ── CAMERA ── */ function bindCam(){ var cb=$('fssmCamBtn'),cc=$('fssmCamCancel'),cs=$('fssmCamShot'),cm=$('fssmCamModal'); if(cb){cb.addEventListener('click',function(e){e.preventDefault();openCam();},false);cb.addEventListener('touchend',function(e){e.preventDefault();openCam();},false);} if(cc)cc.addEventListener('click',closeCam,false); if(cs)cs.addEventListener('click',captureCam,false); if(cm)cm.addEventListener('click',function(e){if(e.target===cm)closeCam();},false); } function openCam(){ var modal=$('fssmCamModal'),st=$('fssmCamStatus'),shot=$('fssmCamShot'),ovl=$('fssmCamOvl'),vid=$('fssmCamVideo'); if(!modal)return; modal.classList.add('show'); if(st){st.textContent='Starting camera...';st.className='fssm-cam-status';} if(shot){shot.disabled=true;shot.classList.remove('ready');} if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){if(st){st.textContent='Camera not supported.';st.classList.add('bad');}return;} navigator.mediaDevices.getUserMedia({video:{facingMode:'user',width:{ideal:1280},height:{ideal:1280}},audio:false}) .then(function(stream){ camStream=stream; if(vid)vid.srcObject=stream; if(st){st.textContent='Align your face in the oval';st.className='fssm-cam-status';} setTimeout(function(){ if(st){st.textContent='Perfect! Tap Capture';st.className='fssm-cam-status good';} if(ovl)ovl.classList.add('good'); if(shot){shot.disabled=false;shot.classList.add('ready');} },1500); }).catch(function(){if(st){st.textContent='Camera access denied.';st.className='fssm-cam-status bad';}}); } function closeCam(){ var modal=$('fssmCamModal'),vid=$('fssmCamVideo'),ovl=$('fssmCamOvl'); if(modal)modal.classList.remove('show'); if(camStream){try{camStream.getTracks().forEach(function(t){t.stop();});}catch(_){}camStream=null;} if(vid)vid.srcObject=null; if(ovl)ovl.classList.remove('good'); } function captureCam(){ var vid=$('fssmCamVideo'); if(!vid||!vid.videoWidth)return; var cv=document.createElement('canvas');cv.width=vid.videoWidth;cv.height=vid.videoHeight; var ctx=cv.getContext('2d');ctx.translate(vid.videoWidth,0);ctx.scale(-1,1);ctx.drawImage(vid,0,0,vid.videoWidth,vid.videoHeight); closeCam();loadPhotoFromSrc(cv.toDataURL('image/jpeg',0.92)); } /* ── ADJUST ── */ function bindAdjust(){ var zone=$('fssmAdjust'); if(!zone)return; var btns=zone.querySelectorAll('.fssm-adj-btn'); function doAdjust(act){ var s=12; if(state.landmarks&&state.displayedW&&state.naturalW)s=Math.max(6,state.landmarks.ipdPx*(state.displayedW/state.naturalW)*0.06); if(act==='up')state.offsetY-=s;else if(act==='down')state.offsetY+=s;else if(act==='left')state.offsetX-=s;else if(act==='right')state.offsetX+=s;else if(act==='bigger')state.scale=Math.min(2.0,state.scale+0.06);else if(act==='smaller')state.scale=Math.max(0.1,state.scale-0.06); placeGlasses(); } for(var i=0;i0)state.scale=Math.max(0.1,Math.min(2.5,state.scale*(nd/t.lastDist)));t.lastDist=nd;placeGlasses();e.preventDefault();}},{passive:false}); box.addEventListener('touchend',function(e){if(e.touches.length===0){t.dragging=false;t.pinching=false;}else if(e.touches.length===1){t.pinching=false;t.dragging=true;t.lastX=e.touches[0].clientX;t.lastY=e.touches[0].clientY;t.lastDist=0;}},{passive:true}); box.addEventListener('touchcancel',function(){t.dragging=false;t.pinching=false;},{passive:true}); } /* ── REDO ── */ function bindRedo(){ var btn=$('fssmRedo'); if(!btn)return; btn.addEventListener('click',function(){ var hero=$('fssmHeroSplit'),stage=$('fssmStage'),photo=$('fssmPhoto'),g=$('fssmGlassesImg'); if(stage)stage.classList.remove('show');if(hero)hero.classList.remove('hide'); if(photo)photo.src='';if(g){g.src='';g.classList.remove('ready');} /* Also make sure scan is fully stopped on redo */ showScan(false); state.landmarks=null;state.shapeScores={};state.offsetX=0;state.offsetY=0;state.scale=1; },false); } /* ── DOWNLOAD ── */ function bindDownload(){ var btn=$('fssmDownloadBtn'); if(!btn)return; btn.addEventListener('click',function(){ var photo=$('fssmPhoto'),gi=$('fssmGlassesImg'),box=$('fssmImageBox'); if(!photo||!photo.src||photo.src===window.location.href){alert('Please upload a photo first!');return;} btn.textContent='Preparing...';btn.style.opacity='0.7'; var br=box.getBoundingClientRect(),dpr=window.devicePixelRatio||1; var cv=document.createElement('canvas');cv.width=br.width*dpr;cv.height=br.height*dpr; var ctx=cv.getContext('2d');ctx.scale(dpr,dpr); ctx.fillStyle='#0a0e14';ctx.fillRect(0,0,br.width,br.height); try{ctx.drawImage(photo,state.photoOffsetX,state.photoOffsetY,state.displayedW,state.displayedH);}catch(e){} /* Use KO_CACHE (knockout-processed) — NOT raw URL to avoid white patti in download */ var processedSrc=KO_CACHE[state.activeShape]||SUNGLASSES_URL[state.activeShape]; if(processedSrc&&gi&&gi.classList.contains('ready')){ var tmp=new Image(); if(processedSrc.indexOf('data:')!==0) tmp.crossOrigin='anonymous'; tmp.onload=function(){try{var gr=gi.getBoundingClientRect();ctx.drawImage(tmp,gr.left-br.left,gr.top-br.top,gr.width,gr.height);}catch(e){}saveCanvas(ctx,cv,br,btn);}; tmp.onerror=function(){saveCanvas(ctx,cv,br,btn);}; tmp.src=processedSrc; }else saveCanvas(ctx,cv,br,btn); },false); } function saveCanvas(ctx,cv,br,btn){ ctx.font='bold 12px Arial,sans-serif';ctx.fillStyle='rgba(0,240,255,0.8)';ctx.textAlign='right';ctx.fillText('profreetools.online',br.width-12,br.height-12); ctx.font='bold 13px Arial,sans-serif';ctx.fillStyle='rgba(103,232,249,0.95)';ctx.textAlign='left';ctx.fillText('Face: '+state.detectedShape,12,26); try{ var dl=document.createElement('a'); dl.setAttribute('href',cv.toDataURL('image/png',0.92)); dl.setAttribute('download','sunglasses-male-'+state.detectedShape.toLowerCase()+'-profreetools.png'); dl.style.display='none';document.body.appendChild(dl);dl.click(); setTimeout(function(){document.body.removeChild(dl);},200); btn.style.opacity='';btn.innerHTML='✓ Downloaded!'; btn.style.background='linear-gradient(135deg,rgba(16,185,129,.25),rgba(5,150,105,.15))'; btn.style.borderColor='#10b981';btn.style.color='#34d399'; setTimeout(function(){btn.innerHTML='⬇ Download My Look';btn.style.background='';btn.style.borderColor='';btn.style.color='';},3000); }catch(e){btn.style.opacity='';btn.innerHTML='📷 Screenshot to Save';setTimeout(function(){btn.innerHTML='⬇ Download My Look';},3000);} } /* ── RESIZE ── */ function bindResize(){ var t=null; window.addEventListener('resize',function(){var stage=$('fssmStage');if(!stage||!stage.classList.contains('show'))return;clearTimeout(t);t=setTimeout(function(){measureDisplay();placeGlasses();},120);},false); } /* ── INIT ── */ function init(){ if(initialized)return; if(!$('fssmShapeTrack')||!$('fssmUpBtn')||!$('fssmCamBtn'))return; initialized=true; renderPills();buildBars();bindUpload();bindCam();bindRedo();bindDownload();bindAdjust();bindTouchGestures();bindResize(); console.log('[FSSM] v13 — laser line permanent fix active.'); } /* Multiple init triggers for WordPress Astra compatibility */ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded',init,false); } else { init(); } /* window.onload — fires after ALL resources loaded (backup for slow WP themes) */ window.addEventListener('load',function(){ if(!initialized) init(); },false); /* setInterval retry — 80 attempts × 150ms = 12 seconds total */ var _t=0,_iv=setInterval(function(){ _t++; if(initialized||_t>80){ clearInterval(_iv); return; } init(); },150); })();