final save
This commit is contained in:
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 |
@@ -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&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>
|
||||
@@ -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'));
|
||||
Reference in New Issue
Block a user