第二版修改
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
1 拍摄的时候,拍摄完前一张图片的时候,需要有一停顿提示用户 拍摄下一张。 这样用户有心理预期。
|
||||
2 报告页面的 "紧塑力 活力感 稳定力" 这个部分还是改回原来的布局,保留点击照片放大的功能就行。
|
||||
+2
-1
@@ -65,6 +65,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
- **预览缩小 15%**:`#cam-video` 按 `scale(.85)` 显示,手机离脸近一些也能把脸装进轮廓;距离阈值随之按 1/0.85 换算(人脸框高/视频高 <0.30 提示「请靠近一点」、>0.68 提示「请远一点」)。
|
||||
- **动态提示**(一次一条,优先级:无人脸 > 距离 > 居中 > 角度 > 光线):「未检测到人脸」「请靠近一点 / 请远一点」「请将脸部移入取景框」(人脸中心偏离中线 >18% 屏宽)、角度类「请向左转头 / 请向右转头 / 请略微抬头 / 请略微低头」(正脸阈值 |yaw|≤15°、|pitch|≤18°,超出时按超出比例更大的一项给方向;侧脸 yaw 28°–75°、|pitch|≤20°)、光线类「请到光线充足处 / 光线过强,请避开强光」(脸部平均亮度 <65 或 >235)。
|
||||
- **合格即拍**:上述条件全部合格并连续维持 450ms(滤掉一闪而过的合格帧,防运动模糊)立即白屏补光抓拍,**无倒计时、无手动快门**。
|
||||
- **两张之间停顿**:正脸、右脸拍完后中央弹出停顿卡约 2s(「正脸拍摄完成 / 下一张:右脸出镜,请向左转头」),停顿期间不检测,结束后再切到下一步并播下一步引导,让用户对下一张有心理预期。
|
||||
- **实时检测**:face-api 从本地 `vendor/faceapi/` 加载(`tinyFaceDetector` + `faceLandmark68Net`),进入拍照页开始加载(状态提示「检测引擎加载中…」)。检测循环 ~150ms 一帧,特征点取自原始帧(预览 CSS 镜像不影响),个别前置帧已镜像的机型由侧脸自校准翻转 `sideYawFlip`;连续漏检 ≥3 帧才报「未检测到人脸」。
|
||||
- **镜像策略**:前置镜头预览镜像(照镜子习惯);后置预览不镜像,侧脸轮廓随之翻转(`.cam-mask.flip`)。**抓帧一律不镜像**,三张照片都是摄像头原始视角。
|
||||
- **白屏补光**:全屏纯白覆盖层瞬间点亮,~300ms 后抓全分辨率帧(原始视角 + 最长边 1280 JPEG),保持 ~300ms 再淡出,同步 `navigator.vibrate(30)`。
|
||||
@@ -77,7 +78,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
## 档案页与报告页
|
||||
|
||||
- **档案页**:出生年月 / 性别 / 所在地区三个滚轮选择器同一风格(`vendor/mobile-select`,MIT),出生年月自动换算年龄;地区为省–市两级联动,数据为 `js/region-data.js`(由 china-division 2.7.0 `pc.json` 生成,另含港澳台与「海外」)。
|
||||
- **报告页**:无返回按钮与页头标题;总分环形图 + 肌肤年龄与「比实际年龄年轻/大 N 岁」;三维度分数 + 进度条(最低分标红并挂「优先关注」);紧塑力/活力感/稳定力各一张卡,卡内子 Tab 切换;点检测图全屏放大,再点或按返回键回到报告。
|
||||
- **报告页**:无返回按钮与页头标题;总分环形图 + 肌肤年龄与「比实际年龄年轻/大 N 岁」;三维度分数 + 进度条(最低分标红并挂「优先关注」);维度详情为原布局——一张卡内紧塑力/活力感/稳定力三个 Tab,每个 Tab 两条「竖排标题 + 左图右文」;点检测图全屏放大,再点或按返回键回到报告。
|
||||
|
||||
## 检测流程与评分渲染
|
||||
|
||||
|
||||
+45
-25
@@ -314,6 +314,31 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
|
||||
}
|
||||
.cam-nudge.show { opacity: 1; transform: translateX(-50%); }
|
||||
|
||||
/* 两张之间的停顿卡:本张完成 + 下一张预告,停顿期间不检测 */
|
||||
.cam-next {
|
||||
position: absolute; left: 50%; top: 50%;
|
||||
transform: translate(-50%, -50%) scale(.94);
|
||||
width: calc(230 * var(--u)); padding: calc(22 * var(--u)) calc(16 * var(--u)) calc(20 * var(--u));
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
background: rgba(0,0,0,.72); border-radius: calc(12 * var(--u));
|
||||
color: #fff; text-align: center;
|
||||
z-index: 3; opacity: 0; pointer-events: none;
|
||||
transition: opacity .2s ease, transform .2s ease;
|
||||
}
|
||||
.cam-next.show { opacity: 1; transform: translate(-50%, -50%); }
|
||||
.cam-next-ok {
|
||||
width: calc(40 * var(--u)); height: calc(40 * var(--u)); border-radius: 50%;
|
||||
background: #34C759; position: relative; margin-bottom: calc(12 * var(--u));
|
||||
}
|
||||
.cam-next-ok::after {
|
||||
content: ""; position: absolute; left: 50%; top: 45%;
|
||||
width: calc(16 * var(--u)); height: calc(8 * var(--u));
|
||||
border: solid #fff; border-width: 0 0 calc(3 * var(--u)) calc(3 * var(--u));
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
.cam-next b { font-size: calc(17 * var(--u)); font-weight: 500; }
|
||||
.cam-next span { margin-top: calc(8 * var(--u)); font-size: calc(14 * var(--u)); color: rgba(255,255,255,.85); line-height: 1.5; }
|
||||
|
||||
/* ═══════════ 08 报告页 ═══════════ */
|
||||
#s-report { background: var(--bg); }
|
||||
.report-body { padding: 0 calc(18 * var(--u)) calc(28 * var(--u)); }
|
||||
@@ -408,40 +433,35 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
|
||||
.dims-note-l1 b { font-weight: 500; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.dims-note-l2 { margin-top: calc(4 * var(--u)); font-size: calc(12 * var(--u)); color: var(--grey); }
|
||||
|
||||
/* 维度区块:大标题 + 卡内子 Tab + 大图 + 说明 */
|
||||
.sec-title {
|
||||
font-size: calc(22 * var(--u)); font-weight: 700; color: var(--ink);
|
||||
text-align: center; letter-spacing: calc(1 * var(--u));
|
||||
margin: calc(26 * var(--u)) 0 calc(16 * var(--u));
|
||||
}
|
||||
.card-detail { padding: calc(14 * var(--u)) calc(30 * var(--u)) calc(22 * var(--u)); }
|
||||
.detail-tabs { display: flex; justify-content: space-between; border-bottom: 1px solid #EFEFEF; }
|
||||
.dtab {
|
||||
position: relative;
|
||||
min-width: calc(80 * var(--u));
|
||||
padding: calc(6 * var(--u)) calc(4 * var(--u)) calc(11 * var(--u));
|
||||
font-size: calc(14.5 * var(--u)); font-weight: 500; color: var(--ink);
|
||||
}
|
||||
/* 维度详情卡(原布局):紧塑力/活力感/稳定力三个 Tab,每个 Tab 两条「竖排标题 + 左图右文」;检测图点击放大 */
|
||||
.card-detail { padding: 0 calc(19 * var(--u)) calc(19 * var(--u)); }
|
||||
.detail-tabs { display: flex; border-bottom: 1px solid #F0F0F0; }
|
||||
.dtab { flex: 1; padding: calc(13 * var(--u)) 0 0; font-weight: 500; color: var(--ink); }
|
||||
.dtab-tx { position: relative; display: inline-block; font-size: calc(10.1 * var(--u)); padding: 0 calc(5 * var(--u)) calc(7 * var(--u)); }
|
||||
.dtab.active { font-weight: 700; }
|
||||
.dtab.active::after {
|
||||
content: ""; position: absolute; left: 0; right: 0; bottom: calc(-3 * var(--u));
|
||||
height: calc(6 * var(--u)); border-radius: calc(3 * var(--u)); background: var(--yellow);
|
||||
.dtab.active .dtab-tx::after {
|
||||
content: ""; position: absolute; left: 50%; transform: translateX(-50%); bottom: -1px;
|
||||
width: calc(78 * var(--u)); height: calc(7 * var(--u)); border-radius: calc(3.5 * var(--u)); background: var(--yellow);
|
||||
}
|
||||
.dpanel { display: none; }
|
||||
.dpanel.active { display: block; animation: fade .25s ease; padding-top: calc(14 * var(--u)); }
|
||||
.dpanel.active { display: block; animation: fade .25s ease; padding-top: calc(19 * var(--u)); }
|
||||
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
|
||||
.ditem { display: flex; gap: calc(9 * var(--u)); align-items: center; }
|
||||
.ditem + .ditem { margin-top: calc(18 * var(--u)); padding-top: calc(18 * var(--u)); border-top: 1px solid #F0F0F0; }
|
||||
.ditem-img {
|
||||
width: calc(213 * var(--u)); height: calc(277 * var(--u));
|
||||
margin: 0 auto calc(14 * var(--u));
|
||||
object-fit: cover; border-radius: calc(6 * var(--u));
|
||||
width: calc(92 * var(--u)); height: calc(96 * var(--u));
|
||||
object-fit: cover; border-radius: calc(6 * var(--u)); flex-shrink: 0;
|
||||
background: #F5F5F5; cursor: zoom-in;
|
||||
}
|
||||
.ditem-img[hidden] { display: none; }
|
||||
.ditem-para {
|
||||
padding-top: calc(12 * var(--u)); border-top: 1px solid #EFEFEF;
|
||||
font-size: calc(13 * var(--u)); color: var(--para); line-height: 1.72;
|
||||
text-align: justify; overflow-wrap: anywhere;
|
||||
.ditem-text { flex: 1; min-width: 0; }
|
||||
.ditem-title {
|
||||
writing-mode: vertical-rl;
|
||||
letter-spacing: calc(4 * var(--u));
|
||||
font-size: calc(11.5 * var(--u)); font-weight: 500; color: var(--ink);
|
||||
margin-right: calc(3.5 * var(--u)); flex-shrink: 0;
|
||||
}
|
||||
.ditem-para { font-size: calc(10.1 * var(--u)); font-weight: 400; color: var(--para); line-height: 1.41; overflow-wrap: anywhere; }
|
||||
|
||||
.vm-notes { font-size: calc(10 * var(--u)); color: var(--grey-3); line-height: 1.7; margin: calc(14 * var(--u)) calc(6 * var(--u)) 0; }
|
||||
.report-disclaimer {
|
||||
|
||||
+58
-44
@@ -118,6 +118,11 @@
|
||||
<p class="cam-status" id="cam-status">检测引擎加载中…</p>
|
||||
<p class="cam-hint" id="cam-hint"></p>
|
||||
<p class="cam-nudge" id="cam-nudge">识别不到?请正对镜头,并到光线充足处</p>
|
||||
<div class="cam-next" id="cam-next" aria-live="polite">
|
||||
<i class="cam-next-ok"></i>
|
||||
<b id="cam-next-done">正脸拍摄完成</b>
|
||||
<span id="cam-next-tip">准备拍摄下一张</span>
|
||||
</div>
|
||||
<div class="cam-guide" id="cam-guide"></div>
|
||||
<video id="cam-video" autoplay playsinline muted></video>
|
||||
<input type="file" id="cam-file" accept="image/*" capture="user" hidden>
|
||||
@@ -170,52 +175,61 @@
|
||||
<div class="dims-note" id="dims-note"><p class="dims-note-l1">近期优先关注:<b>---</b></p></div>
|
||||
</div>
|
||||
|
||||
<!-- 三个维度各一张卡:卡内子 Tab 切换所辖两个分类;检测图点击放大 -->
|
||||
<h2 class="sec-title">紧塑力</h2>
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs">
|
||||
<button class="dtab active" data-tab="0">轮廓</button>
|
||||
<button class="dtab" data-tab="1">皱纹</button>
|
||||
<div class="detail-tabs" id="detail-tabs">
|
||||
<button class="dtab active" data-tab="0"><span class="dtab-tx">紧塑力</span></button>
|
||||
<button class="dtab" data-tab="1"><span class="dtab-tx">活力感</span></button>
|
||||
<button class="dtab" data-tab="2"><span class="dtab-tx">稳定力</span></button>
|
||||
</div>
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<img id="ditem-img1" src="assets/img/det-1.jpg" loading="lazy" class="ditem-img" alt="轮廓检测图">
|
||||
<p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
<div class="dpanel" data-panel="1">
|
||||
<img id="ditem-img2" src="assets/img/det-2.jpg" loading="lazy" class="ditem-img" alt="皱纹检测图">
|
||||
<p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sec-title">活力感</h2>
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs">
|
||||
<button class="dtab active" data-tab="0">光滑</button>
|
||||
<button class="dtab" data-tab="1">肤色水润</button>
|
||||
</div>
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<img id="viz-smooth" loading="lazy" class="ditem-img" alt="光滑检测标记图" hidden>
|
||||
<p class="ditem-para" id="tx-smooth">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
<div class="dpanel" data-panel="1">
|
||||
<img id="viz-tone" loading="lazy" class="ditem-img" alt="肤色色沉图" hidden>
|
||||
<p class="ditem-para" id="tx-tone">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sec-title">稳定力</h2>
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs">
|
||||
<button class="dtab active" data-tab="0">敏感</button>
|
||||
<button class="dtab" data-tab="1">痘痘</button>
|
||||
</div>
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<img id="viz-sens" loading="lazy" class="ditem-img" alt="敏感红区图" hidden>
|
||||
<p class="ditem-para" id="tx-sens">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
<div class="dpanel" data-panel="1">
|
||||
<img id="viz-acne" loading="lazy" class="ditem-img" alt="痘痘检测标记图" hidden>
|
||||
<p class="ditem-para" id="tx-acne">检测完成后显示分析结果。</p>
|
||||
<div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<div class="ditem">
|
||||
<h3 class="ditem-title">轮廓</h3>
|
||||
<img id="ditem-img1" src="assets/img/det-1.jpg" loading="lazy" class="ditem-img" alt="轮廓检测图">
|
||||
<div class="ditem-text">
|
||||
<p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ditem">
|
||||
<h3 class="ditem-title">皱纹</h3>
|
||||
<img id="ditem-img2" src="assets/img/det-2.jpg" loading="lazy" class="ditem-img" alt="皱纹检测图">
|
||||
<div class="ditem-text">
|
||||
<p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dpanel" data-panel="1">
|
||||
<div class="ditem">
|
||||
<h3 class="ditem-title">光滑</h3>
|
||||
<img id="viz-smooth" loading="lazy" class="ditem-img" alt="光滑检测标记图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="ditem-para" id="tx-smooth">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ditem">
|
||||
<h3 class="ditem-title">肤色水润</h3>
|
||||
<img id="viz-tone" loading="lazy" class="ditem-img" alt="肤色色沉图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="ditem-para" id="tx-tone">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dpanel" data-panel="2">
|
||||
<div class="ditem">
|
||||
<h3 class="ditem-title">敏感</h3>
|
||||
<img id="viz-sens" loading="lazy" class="ditem-img" alt="敏感红区图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="ditem-para" id="tx-sens">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ditem">
|
||||
<h3 class="ditem-title">痘痘</h3>
|
||||
<img id="viz-acne" loading="lazy" class="ditem-img" alt="痘痘检测标记图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="ditem-para" id="tx-acne">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+23
-8
@@ -711,10 +711,14 @@
|
||||
// 左脸出镜=头向自己的右侧转(target 'left',产物作 left_image)。
|
||||
// mask 为 index.html 取景轮廓编号(1=左脸出镜 耳在画面左,2=右脸出镜 耳在画面右,均按镜像预览绘制)
|
||||
var CAM_STEPS = [
|
||||
{ step: '1/3<br>请正对屏幕', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front', mask: 0 },
|
||||
{ step: '2/3<br>请右脸出镜', guides: ['右脸居中', '取下眼镜', '光线充足'], target: 'right', mask: 2 },
|
||||
{ step: '3/3<br>请左脸出镜', guides: ['左脸居中', '取下眼镜', '光线充足'], target: 'left', mask: 1 }
|
||||
{ step: '1/3<br>请正对屏幕', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front', mask: 0,
|
||||
done: '正脸拍摄完成' },
|
||||
{ step: '2/3<br>请右脸出镜', guides: ['右脸居中', '取下眼镜', '光线充足'], target: 'right', mask: 2,
|
||||
done: '右脸拍摄完成', preview: '下一张:右脸出镜<br>请向左转头' },
|
||||
{ step: '3/3<br>请左脸出镜', guides: ['左脸居中', '取下眼镜', '光线充足'], target: 'left', mask: 1,
|
||||
done: '左脸拍摄完成', preview: '下一张:左脸出镜<br>请向右转头' }
|
||||
];
|
||||
var NEXT_PAUSE_MS = 2000; // 两张之间的停顿:给用户「下一张要拍什么」的心理预期
|
||||
function photosByView(ph) { // 拍摄顺序 → 送检/报告/历史统一口径 [正脸, 左脸, 右脸]
|
||||
var at = {};
|
||||
CAM_STEPS.forEach(function (s, i) { at[s.target] = ph[i]; });
|
||||
@@ -729,6 +733,7 @@
|
||||
.map(function (g) { return '<span>' + g + '</span>'; })
|
||||
.join('<i></i>');
|
||||
$('#cam-mask').setAttribute('data-step', cfg.mask); // 蒙版按步骤切换轮廓
|
||||
$('#cam-next').classList.remove('show');
|
||||
applyMirror();
|
||||
resetDetectState();
|
||||
voice.play(VOICE.step[camStep]); // 步骤引导语音
|
||||
@@ -739,20 +744,30 @@
|
||||
|
||||
// 照片就绪(已通过校验)后推进流程:存图→「拍摄完成」→300ms 过渡→下一步;三步严格按顺序,本步合格才进下一步
|
||||
function onPhotoCaptured(dataUrl) {
|
||||
shutterLock = true; // 推进窗口上锁,直到下一步/送检
|
||||
shutterLock = true; // 推进窗口上锁(停顿期间不检测、不接受快门),直到下一步/送检
|
||||
photos[camStep] = dataUrl;
|
||||
voice.play(VOICE.shot, { cut: true }); // 「拍摄完成」:已拍下,本步未播完的引导作废
|
||||
var seq = camSeq;
|
||||
var seq = camSeq, hasNext = camStep < CAM_STEPS.length - 1;
|
||||
if (hasNext) {
|
||||
// 停顿卡:本张完成 + 下一张拍什么、往哪转;停顿结束再切到下一步(播下一步引导语音)
|
||||
setHint('');
|
||||
hideNudge();
|
||||
clearTimeout(nudgeTimer);
|
||||
setMask('');
|
||||
$('#cam-next-done').textContent = CAM_STEPS[camStep].done;
|
||||
$('#cam-next-tip').innerHTML = CAM_STEPS[camStep + 1].preview;
|
||||
$('#cam-next').classList.add('show');
|
||||
}
|
||||
setTimeout(function () {
|
||||
if (seq !== camSeq) return; // 期间已退出/重新开始:放弃本次推进
|
||||
if (camStep < CAM_STEPS.length - 1) {
|
||||
if (hasNext) {
|
||||
camStep++;
|
||||
renderCamStep(); // 内含检测状态机重置
|
||||
renderCamStep(); // 内含停顿卡收起与检测状态机重置
|
||||
shutterLock = false; // 进入下一步即解锁
|
||||
} else {
|
||||
submitPhotos(seq); // 三张拍完:送检
|
||||
}
|
||||
}, 300);
|
||||
}, hasNext ? NEXT_PAUSE_MS : 300);
|
||||
}
|
||||
|
||||
// 白屏补光抓拍:全屏纯白瞬间点亮→约300ms 后抓全分辨率帧→保持300ms 再淡出(总 600–900ms)
|
||||
|
||||
Reference in New Issue
Block a user