final save

This commit is contained in:
xsl
2026-10-15 22:16:01 +08:00
parent 49db1a6f5b
commit 98d6189f5c
14 changed files with 721 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

+129
View File
@@ -0,0 +1,129 @@
<!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>
+36
View File
@@ -0,0 +1,36 @@
// Loopback-only, read-only preview. Private downloaded files stay outside Git.
// H5_PRIVATE_CASE_DIR=/private/case node h5/test/private-case-preview.mjs
import http from 'node:http';import fs from 'node:fs';import path from 'node:path';
import{fileURLToPath}from'node:url';
const dir=process.env.H5_PRIVATE_CASE_DIR;
if(!dir)throw Error('Set H5_PRIVATE_CASE_DIR to the explicitly downloaded private case');
const root=fileURLToPath(new URL('../',import.meta.url));
const raw=JSON.parse(fs.readFileSync(path.join(dir,'raw.json')));
const photos=['front','left','right'].map(k=>'data:image/jpeg;base64,'+fs.readFileSync(path.join(dir,k+'.jpg')).toString('base64'));
const fixture={result:raw.result,side:{left:raw.left_side_result,right:raw.right_side_result},face_rectangle:raw.face_rectangle,photos,
realAge:Number(process.env.H5_CASE_REAL_AGE||26),skinType:process.env.H5_CASE_SKIN_TYPE||'干性肌肤'};
fixture.wrinkle_views={version:'independent-side-wrinkles-v1'};
for(const view of ['left','right']){
try{const data=JSON.parse(fs.readFileSync(path.join(dir,view+'-wrinkles.json')));fixture.wrinkle_views[view]={status:'ready',result:data.result,face_rectangle:data.face_rectangle};}catch{}
}
const mime={'.html':'text/html; charset=utf-8','.js':'application/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json','.png':'image/png','.jpg':'image/jpeg','.svg':'image/svg+xml','.woff2':'font/woff2','.webp':'image/webp','.mp3':'audio/mpeg'};
const server=http.createServer((req,res)=>{
res.setHeader('Cache-Control','no-store');res.setHeader('X-Robots-Tag','noindex, nofollow');
if(req.method!=='GET'){res.writeHead(405);return res.end();}
let pathname;try{pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);}catch{res.writeHead(400);return res.end();}
if(pathname==='/api/auth/session'||pathname==='/api/auth/config'){
res.setHeader('Content-Type','application/json');return res.end(JSON.stringify({ok:true,wechat:false,user:{id:'private-case-preview',kind:'browser'}}));
}
if(pathname==='/assets/mock/report-fixture.js'){
res.setHeader('Content-Type',mime['.js']);return res.end('window.MOCK_FIXTURE='+JSON.stringify(fixture)+';');
}
const file=path.resolve(root,'.'+(pathname==='/'?'/index.html':pathname));
if(!file.startsWith(root)||/^\/(data|api|test|lib)(\/|$)/.test(pathname)||pathname==='/server.js'||pathname.split('/').some(s=>s.startsWith('.'))){res.writeHead(404);return res.end();}
const type=mime[path.extname(file)];if(!type){res.writeHead(404);return res.end();}
try{
let data=fs.readFileSync(file);
if(pathname==='/js/app.js')data=data.toString().replace("realAge: +($('#f-age').value || 0) || 28",'realAge: window.MOCK_FIXTURE.realAge').replace("skinType: SKINS[skinIndex].name\n }), fx ?",'skinType: window.MOCK_FIXTURE.skinType\n }), fx ?');
res.setHeader('Content-Type',type);res.end(data);
}catch{res.writeHead(404);res.end();}
});
server.listen(Number(process.env.PORT||18125),'127.0.0.1',()=>console.log('Private case preview ready on loopback port '+server.address().port+'; GET only; no upstream calls or writes'));