Files
juzi/h5/contour-preview.html
T
2026-10-15 22:16:01 +08:00

130 lines
9.1 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>轮廓与皱纹三视图渐变 · 测试预览</title>
<style>
* { box-sizing:border-box; }
html,body { margin:0; height:100%; background:#161616; font-family:system-ui,sans-serif; }
body { display:flex; flex-direction:column; }
header { flex:none; min-height:48px; background:#fff; display:flex; align-items:center; justify-content:center; gap:12px; padding:8px 12px; font-size:12px; }
header span { color:#666; }
button { border:0; border-radius:5px; background:#F5F078; color:#282828; padding:7px 12px; cursor:pointer; }
iframe { display:block; border:0; width:100%; flex:1; min-height:0; }
#error { display:none; color:#fff; padding:20px; text-align:center; }
</style>
</head>
<body>
<header><strong>轮廓/皱纹预览</strong><span>每3.8秒切换 · 渐变0.8秒</span><button id="toggle" disabled>暂停</button></header>
<p id="error" role="alert"></p>
<iframe id="report" title="测试报告:正脸、左脸、右脸自动渐变" src="./?mock=1&amp;screen=report"></iframe>
<script>
// Standalone local demo. Only alters this iframe's DOM; no analysis or save calls.
(function () {
var observer=null, paused=false, disposed=false, groups=[];
var iframe=document.getElementById('report'),toggle=document.getElementById('toggle');
var names=['正脸','左脸','右脸'];
// Repeating cycle: front → left → front → right → front → left …
var sequence=[0,1,0,2];
// Same face-centered crop formula as app.js vizFaceCrop. The demo's
// three sample photos share dimensions/framing; use their reference box.
function focusFace(img,referenceBox,referencePhoto,zoomFraction){
var width=img.naturalWidth,height=img.naturalHeight;
var sx=width/referencePhoto.naturalWidth,sy=height/referencePhoto.naturalHeight;
var rectangle=referenceBox&&{left:referenceBox.left*sx,top:referenceBox.top*sy,width:referenceBox.width*sx,height:referenceBox.height*sy};
if(!rectangle||rectangle.left<0||rectangle.top<0||rectangle.width<=0||rectangle.height<=0||rectangle.left+rectangle.width>width||rectangle.top+rectangle.height>height)return;
var ratio=215/278;
var ch=Math.min(Math.max(rectangle.height*1.55,rectangle.width*1.18/ratio),height,width/ratio),cw=ch*ratio;
if(cw<rectangle.width||ch<rectangle.height)return;
var left=Math.max(0,Math.min(width-cw,rectangle.left+rectangle.width/2-cw/2));
var top=Math.max(0,Math.min(height-ch,rectangle.top-rectangle.height*.3));
if(top+ch<rectangle.top+rectangle.height)top=rectangle.top+rectangle.height-ch;
if(zoomFraction===.5){
// Half of the added magnification, not half of the resulting scale.
// Start with a centered, contain-fit crop at the same frame ratio.
var baseHeight=Math.max(height,width/ratio),baseWidth=baseHeight*ratio;
var baseScale=1/baseWidth,fullScale=1/cw;
var halfWidth=1/(baseScale+(fullScale-baseScale)*.5),halfHeight=halfWidth/ratio;
var centerX=width*.5+(left+cw*.5-width*.5)*.5;
var centerY=height*.5+(top+ch*.5-height*.5)*.5;
cw=halfWidth;ch=halfHeight;left=centerX-cw*.5;top=centerY-ch*.5;
img.dataset.zoomFraction='0.5';
}
img.style.inset='auto';img.style.width=(width/cw*100)+'%';img.style.height=(height/ch*100)+'%';
img.style.left=(-left/cw*100)+'%';img.style.top=(-top/ch*100)+'%';
img.style.objectFit='fill';img.style.maxWidth='none';img.dataset.crop='face';
}
function stop(group){clearInterval(group.timer);group.timer=null;}
function next(group){
var previous=group.layers[group.current];
previous.style.zIndex='1';
group.step=(group.step+1)%sequence.length;
group.current=sequence[group.step];
var incoming=group.layers[group.current];
incoming.style.zIndex='2';
incoming.classList.add('active');
// Keep the old image fully opaque underneath the incoming fade. This
// yields a real cross-dissolve without flashing the white background.
clearTimeout(group.fadeTimer);
group.fadeTimer=setTimeout(function(){previous.classList.remove('active');previous.style.zIndex='0';},850);
group.caption.textContent=names[group.current]+' · '+(group.current+1)+' / 3';
}
function sync(){groups.forEach(function(group){stop(group);if(!disposed&&!paused&&group.inView&&!document.hidden&&group.ready)group.timer=setInterval(function(){next(group);},3800);});}
toggle.onclick=function(){paused=!paused;toggle.textContent=paused?'继续播放':'暂停';toggle.setAttribute('aria-pressed',String(paused));sync();};
document.addEventListener('visibilitychange',sync);
window.addEventListener('pagehide',function(){disposed=true;groups.forEach(function(group){stop(group);clearTimeout(group.fadeTimer);group.layers.forEach(function(img,i){img.classList.toggle('active',i===group.current);img.style.zIndex=i===group.current?'2':'0';});});});
window.addEventListener('pageshow',function(){disposed=false;sync();});
iframe.onload=async function(){
groups.forEach(function(group){stop(group);clearTimeout(group.fadeTimer);});if(observer)observer.disconnect();groups=[];
try{
var doc=iframe.contentDocument,card=doc.querySelector('.card-detail');
var referencePhoto=doc.createElement('img');
await new Promise(function(resolve,reject){referencePhoto.onload=resolve;referencePhoto.onerror=reject;referencePhoto.src=doc.defaultView.MOCK_FIXTURE.photos[0];});
// Preserve the real front-view wrinkle composite rendered by the
// report (including its original dimming and detection overlay).
var wrinkleImage=doc.getElementById('ditem-img2');
await new Promise(function(resolve,reject){
var started=Date.now(),poll=setInterval(function(){
if(wrinkleImage&&!wrinkleImage.hidden&&wrinkleImage.complete&&wrinkleImage.naturalWidth){clearInterval(poll);resolve();}
else if(Date.now()-started>8000){clearInterval(poll);reject(new Error('皱纹浮层未加载'));}
},50);
});
var style=doc.createElement('style');
style.textContent='.contour-demo-frame .ditem-img:not(.demo-face),.contour-demo-frame .viz-missing{display:none!important}.contour-demo-frame .demo-face{position:absolute;inset:0;display:block;opacity:0;transition:opacity .8s linear!important;pointer-events:none;object-fit:cover}.contour-demo-frame .demo-face.active{opacity:1;pointer-events:auto}.contour-demo-caption{text-align:center;color:#707070;font-size:11px;line-height:20px;margin:-4px 0 12px}';
doc.head.appendChild(style);
var sources=['front','left','right'];
var ready=[];
card.querySelectorAll('.viz-frame').forEach(function(frame){
frame.classList.add('contour-demo-frame');
var isWrinkles=!!frame.querySelector('#ditem-img2');
var group={frame:frame,layers:[],current:0,step:0,inView:false,ready:false,timer:null,fadeTimer:null};
group.caption=doc.createElement('p');group.caption.className='contour-demo-caption';group.caption.textContent='正脸 · 1 / 3';frame.after(group.caption);
groups.push(group);
sources.forEach(function(source,i){ready.push(new Promise(function(resolve,reject){
var img=doc.createElement('img');img.className='ditem-img demo-face'+(i===0?' active':'');img.alt=names[i]+'拍摄照片';
var useWrinkleOverlay=isWrinkles&&i===0;
img.onload=function(){
if(isWrinkles&&!useWrinkleOverlay)focusFace(img,doc.defaultView.MOCK_FIXTURE.face_rectangle,referencePhoto);
if(!isWrinkles)focusFace(img,doc.defaultView.MOCK_FIXTURE.face_rectangle,referencePhoto,.5);
resolve();
};img.onerror=function(){reject(new Error('示例照片加载失败'));};
img.dataset.wrinkleOverlay=useWrinkleOverlay?'original-front':'none';
frame.appendChild(img);group.layers.push(img);
img.src=useWrinkleOverlay?wrinkleImage.src:(doc.defaultView.MOCK_FIXTURE.photos[i]||'assets/mock/contour-demo/'+source+'.jpg');
}));});
});
await Promise.all(ready);if(disposed)return;
toggle.disabled=false;
card.previousElementSibling.scrollIntoView({block:'start'});
groups.forEach(function(group){group.ready=true;});
observer=new IntersectionObserver(function(entries){entries.forEach(function(entry){var group=groups.find(function(g){return g.frame===entry.target;});group.inView=entry.isIntersecting&&entry.intersectionRatio>=.2;});sync();},{threshold:.2});
groups.forEach(function(group){observer.observe(group.frame);});
sync();
}catch(e){var error=document.getElementById('error');error.style.display='block';error.textContent='预览未就绪,请刷新重试。';}
};
})();
</script>
</body>
</html>