"use strict"; // Reference geometry is in input-image pixels; no screen-vertical assumptions. window.hairlineReferenceBox = function(points) { const p = id => { const value = points[String(id)]; if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y)) throw new Error(`缺少 ${id} 号关键点`); return [value.x, value.y]; }; const add = (a,b) => [a[0]+b[0],a[1]+b[1]]; const sub = (a,b) => [a[0]-b[0],a[1]-b[1]]; const mul = (a,s) => [a[0]*s,a[1]*s]; const dot = (a,b) => a[0]*b[0]+a[1]*b[1]; const brow=p(9),chin=p(152),left=p(162),right=p(389); const delta=sub(chin,brow),length=Math.hypot(...delta); if(length<1)throw new Error("9→152 方向无法确定"); const up=mul(delta,-1/length),horizontal=[-up[1],up[0]],height=.7*length; const sidePoint=(anchor,direction,level)=>{ const denominator=dot(direction,up); if(Math.abs(denominator)<1e-6)throw new Error("参考框侧边与底边平行"); return add(anchor,mul(direction,(level-dot(sub(anchor,brow),up))/denominator)); }; const corners=[sidePoint(left,up,0),sidePoint(right,up,0), sidePoint(right,up,height),sidePoint(left,up,height)]; if(dot(sub(corners[1],corners[0]),horizontal)<=0)throw new Error("参考框左右位置异常"); return {corners,height,faceLength:length, landmarks:{9:brow,152:chin,162:left,389:right}}; }; window.clipHairlineToReferenceBox = function(image, points) { const box=window.hairlineReferenceBox(points); const width=image.naturalWidth||image.width,height=image.naturalHeight||image.height; if(!width||!height)throw new Error("发际线图片尚未加载"); const canvas=document.createElement("canvas");canvas.width=width;canvas.height=height; const ctx=canvas.getContext("2d"); ctx.beginPath();box.corners.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p));ctx.closePath(); ctx.clip();ctx.drawImage(image,0,0,width,height); return canvas; }; window.drawHairlineReferenceBox = function(ctx, points, crop, width, height) { const box=window.hairlineReferenceBox(points); const [x0,y0,x1,y1]=crop; const screen=p=>[(p[0]-x0)/(x1-x0)*width,(p[1]-y0)/(y1-y0)*height]; const color="#e33399"; ctx.save();ctx.strokeStyle=color;ctx.lineWidth=Math.max(2.5,width/280);ctx.lineJoin="round"; ctx.beginPath();box.corners.forEach((p,i)=>{const q=screen(p);i?ctx.lineTo(...q):ctx.moveTo(...q);});ctx.closePath();ctx.stroke(); ctx.lineWidth=1.4;ctx.setLineDash([7,6]);ctx.beginPath();ctx.moveTo(...screen(box.landmarks[9]));ctx.lineTo(...screen(box.landmarks[152]));ctx.stroke();ctx.setLineDash([]); const fontSize=Math.max(13,width/43);ctx.font=`600 ${fontSize}px sans-serif`; for(const id of [9,152,162,389]){ const [x,y]=screen(box.landmarks[id]); if(x<0||x>width||y<0||y>height)continue; ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,Math.max(3,width/160),0,Math.PI*2);ctx.fill(); const label=String(id),tw=ctx.measureText(label).width; const tx=Math.max(3,Math.min(width-tw-8,x+7)),ty=Math.max(fontSize+4,Math.min(height-5,y-7)); ctx.fillStyle="rgba(255,255,255,.9)";ctx.fillRect(tx-3,ty-fontSize,tw+6,fontSize+4);ctx.fillStyle="#a51868";ctx.fillText(label,tx,ty); } const top=screen([(box.corners[2][0]+box.corners[3][0])/2,(box.corners[2][1]+box.corners[3][1])/2]); const text="70% · 9→152",tw=ctx.measureText(text).width; const tx=Math.max(4,Math.min(width-tw-6,top[0]-tw/2)),ty=Math.max(fontSize+4,top[1]-8); ctx.fillStyle="rgba(255,255,255,.9)";ctx.fillRect(tx-3,ty-fontSize,tw+6,fontSize+4);ctx.fillStyle="#a51868";ctx.fillText(text,tx,ty); ctx.restore(); };