130 lines
9.1 KiB
HTML
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&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>
|