/* ================================================================
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);
})();