feat: 拍摄流程对齐 Figma(正→左→右顺序/描摹轮廓蒙版/正脸只提示回正) + 报告页按 Figma 08 重排
- 拍照顺序 正脸→左脸→右脸:步骤文案/语音文件(step2-leftface/step3-rightface)/payload 归属对齐 - 蒙版轮廓由 Figma 拍照示意位图 alpha 镂空区逐像素描摹生成(0 正脸含耳/1 左脸鼻朝左/2 右脸鼻朝右),框外纯黑 - 侧脸步预览与抓帧不镜像(左右不再迷惑),HeadPose 侧脸自校准阈值 30 帧防误翻转 - 语音:「保持不动」播完再进倒计时(afterCurrent 接续,不再拦腰切断) - 正脸步角度不合格只提示「请正对屏幕」,阈值放宽 |yaw|≤15°/|pitch|≤18°;转头提示仅侧脸步 - 抓帧改完整帧(修 Face++ INVALID_IMAGE_FACE) - 报告页按 Figma 08:内联白底顶栏(返回+标题)、总分双独立白卡(30px 大数字+15px 单位后缀)、 维度短竖分隔线+最低分标红、Tab 黄条、免责左对齐、护理建议去 tag 加粗+收尾句、 产品大标题移出白卡、COREⅡ 换 Figma 导出 SVG 字标、产品图 151×174、卡圆角 10px - 精华卡判据措辞用户化(均分档→综合均分/最差项约束→最低分维度/年龄组档→年龄段,引擎值不动) - 修报告页滚动 P0(去 position:relative 覆写),文档同步(语音文档/README)
@@ -20,10 +20,10 @@
|
||||
| 文件名 | 文案 | 触发时机 | 时长 |
|
||||
|---|---|---|---|
|
||||
| `step1-front.mp3` | 请单人正脸出镜,面部对准取景框 | 进入拍照页 1/3 正脸步 | ≈3s |
|
||||
| `step2-rightface.mp3` | 请保持单人出镜,慢慢向左转头,露出右脸 | 进入 2/3 右脸出镜步 | ≈4s |
|
||||
| `step3-leftface.mp3` | 请慢慢向右转头,露出左脸 | 进入 3/3 左脸出镜步 | ≈3s |
|
||||
| `step2-leftface.mp3` | 请慢慢向右转头,露出左脸 | 进入 2/3 左脸出镜步 | ≈3s |
|
||||
| `step3-rightface.mp3` | 请保持单人出镜,慢慢向左转头,露出右脸 | 进入 3/3 右脸出镜步 | ≈4s |
|
||||
|
||||
> 语义说明:2/3 步需要画面中看到「右脸」= 用户头向**左**转;3/3 步看到「左脸」= 头向**右**转。语音引导的是用户动作,与取景蒙版方向一致,不要改写成"露出左侧/右侧脸"这类歧义表述。
|
||||
> 语义说明:拍摄顺序为正脸 → 左脸 → 右脸。2/3 步需要画面中看到「左脸」= 用户头向**右**转;3/3 步看到「右脸」= 头向**左**转。语音引导的是用户动作,与取景蒙版方向一致,不要改写成"露出左侧/右侧脸"这类歧义表述。
|
||||
|
||||
### B. 姿态提示(实时检测不通过时播,同一提示 4 秒内不重复)
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
| `hint-noface.mp3` | 未检测到人脸,请正对镜头 | 画面无人脸 | ≈3s |
|
||||
| `hint-near.mp3` | 请靠近一点 | 人脸框过高占比 <35%(太远) | ≈1.5s |
|
||||
| `hint-far.mp3` | 请远一点 | 占比 >80%(太近) | ≈1.5s |
|
||||
| `hint-front.mp3` | 请正对屏幕 | 正脸步角度不合格 | ≈1.5s |
|
||||
| `hint-turn-left.mp3` | 请向左转头 | 需要头向左转修正角度时 | ≈1.5s |
|
||||
| `hint-turn-right.mp3` | 请向右转头 | 需要头向右转修正角度时 | ≈1.5s |
|
||||
| `hint-front.mp3` | 请正对屏幕 | 正脸步角度不合格(正脸步一律只提示回正,不播转头/抬头低头) | ≈1.5s |
|
||||
| `hint-turn-left.mp3` | 请向左转头 | 仅侧脸步:需要头向左转修正角度时 | ≈1.5s |
|
||||
| `hint-turn-right.mp3` | 请向右转头 | 仅侧脸步:需要头向右转修正角度时 | ≈1.5s |
|
||||
| `hint-hold.mp3` | 保持不动 | 姿态已合格、进入稳定确认 | ≈1.5s |
|
||||
|
||||
### C. 拍摄与流程
|
||||
@@ -51,7 +51,7 @@
|
||||
1. **清单制**:语音文件配 `assets/voice/manifest.json`(格式 `{"files":["step1-front","hint-near",...]}`,内容为已生成的文件名去 .mp3)。前端只播清单里声明的条目;清单或条目缺失时该条静默跳过——语音文件可分批补齐,每次补齐后更新清单。
|
||||
2. **浏览器自动播放限制**:首次播放必须发生在用户手势链路里。前端在任意按钮点击时初始化并 resume WebAudio(点击「开始检测/继续/开始」即完成解锁),后续步骤提示均为程序触发。
|
||||
3. **同条去重**:同一文件名 4 秒内不重复播(姿态提示会随检测帧反复触发)。
|
||||
4. **不叠加**:新提示开播前停掉上一条未播完的(单通道),倒计时与拍摄完成音除外(允许叠在提示音上)。
|
||||
4. **不叠加**:新提示开播前停掉上一条未播完的(单通道);例外:倒计时「三、二、一」不在播报中途拦腰切入——姿态达标后先等「保持不动」等在播提示音自然收尾,再同步起播倒计时语音与画面数字(`voice.afterCurrent` 接续,期间姿态被破坏则取消倒计时)。
|
||||
5. **不打断原则**:分析中/失败提示开播时,拍照页已离开,步骤与姿态提示不再触发。
|
||||
6. **静音开关**:V1.0 暂不提供 UI 开关,跟随系统音量与静音键;后续如加开关,在拍照页右上角放小喇叭图标,状态存 localStorage。
|
||||
|
||||
|
||||
@@ -51,13 +51,15 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
|
||||
## 拍照交互(引导式自动三连拍)
|
||||
|
||||
三步:1/3 正脸(target `front`)→ 2/3 右脸出镜(用户头向左转,target `right`,产物作 `right_image`)→ 3/3 左脸出镜(头向右转,target `left`,产物作 `left_image`)。
|
||||
三步:1/3 正脸(target `front`)→ 2/3 左脸出镜(用户头向右转,target `left`,产物作 `left_image`)→ 3/3 右脸出镜(头向左转,target `right`,产物作 `right_image`)。
|
||||
|
||||
- **步骤圆点**:顶部三点,当前步高亮、完成步绿色打勾。
|
||||
- **取景蒙版**:内联 SVG(`#cam-mask`)按步骤切换轮廓——正脸步为正脸线框(含耳部),侧脸步为对应方向的侧脸剪影(鼻朝向与转头方向一致);框外 58% 黑色半透明压暗(SVG mask 镂空),轮廓线 2.5px 半透明白。未检测到人脸时轮廓呼吸动画(1.8s);姿态 ok 时轮廓变绿并停止动效。
|
||||
- **实时检测**:face-api 从本地 `vendor/faceapi/` 加载(`tinyFaceDetector` + `faceLandmark68Net`)。进入拍照页即开始加载模型(状态提示"检测引擎加载中…"),加载完成前快门为手动模式。检测循环 ~150ms 一帧,`detectSingleFace(video, TinyFaceDetectorOptions({inputSize:416, scoreThreshold:0.3})).withFaceLandmarks()` 后拿 68 特征点调 `HeadPose.check(pos, target, /* mirrored= */true)`;连续漏检 ≥3 帧才报"未检测到人脸"(容忍单帧抖动);人脸框高度占比 <26% 提示"请靠近一点"、>58% 提示"请远一点"(以取景框≈42% 屏高标定)。提示一次一条,优先级:无人脸 > 距离 > 角度。
|
||||
- **倒计时自动拍**:姿态 ok 连续维持 ≥600ms 进入 3s 倒计时(中央大数字,轮廓保持绿色);倒计时中任一帧不 ok 立即取消并回到对应提示。倒计时结束进入白屏补光。
|
||||
- **白屏补光**:全屏纯白覆盖层瞬间点亮,~300ms 后抓全分辨率帧(镜像 + 最长边 1280 JPEG),保持 ~300ms 再淡出(总 600–900ms),同步 `navigator.vibrate(30)`。
|
||||
- **取景蒙版**:内联 SVG(`#cam-mask`)按步骤切换轮廓——正脸步为正脸剪影(含耳部),侧脸步为对应方向的侧脸剪影(鼻朝向与转头方向一致);**轮廓路径由 Figma 拍照示意图(`design/assets/images/*-拍照示意.png` 的 alpha 镂空区)逐像素描摹生成**,等比缩放到 375×812 viewBox;框外纯黑不透明(SVG mask 镂空,对齐 Figma:只有轮廓内显示实时画面),轮廓线 2.5px 半透明白。未检测到人脸时轮廓呼吸动画(1.8s);姿态 ok 时轮廓变绿并停止动效。
|
||||
- **正脸步提示口径**:正脸步角度不合格一律只提示「请正对屏幕」(不再提示左右转头/抬头低头),阈值 |yaw|≤15°、|pitch|≤18°(2026-09-28 产品要求放宽);侧脸步仍按方向提示「请向左/右转头」。
|
||||
- **实时检测**:face-api 从本地 `vendor/faceapi/` 加载(`tinyFaceDetector` + `faceLandmark68Net`)。进入拍照页即开始加载模型(状态提示"检测引擎加载中…"),加载完成前快门为手动模式。检测循环 ~150ms 一帧,`detectSingleFace(video, TinyFaceDetectorOptions({inputSize:416, scoreThreshold:0.3})).withFaceLandmarks()` 后拿 68 特征点调 `HeadPose.check(pos, target, mirrored)`——mirrored 正脸步跟随镜头(前置 true/后置 false),侧脸步恒 false(预览不镜像,特征点即原始视角;个别前置帧已镜像的机型由侧脸自校准翻转 `sideYawFlip`,只动判定不动画面);连续漏检 ≥3 帧才报"未检测到人脸"(容忍单帧抖动);人脸框高度占比 <26% 提示"请靠近一点"、>58% 提示"请远一点"(以取景框≈42% 屏高标定)。提示一次一条,优先级:无人脸 > 距离 > 角度。
|
||||
- **倒计时自动拍**:姿态 ok 连续维持 ≥600ms 后,先等「保持不动」等在播提示音自然收尾(画面保持绿框+保持不动),再进 3s 倒计时(中央大数字,轮廓保持绿色,数字与「三、二、一」语音同步起播,不再拦腰切断);倒计时中任一帧不 ok 立即取消并回到对应提示。倒计时结束进入白屏补光。
|
||||
- **镜像策略**:正脸步前置镜像(自拍习惯),侧脸步预览与抓帧均不镜像(所见即摄像头原始视角,轮廓鼻朝向与转头方向一致,左右不再迷惑);后置镜头全程不镜像。
|
||||
- **白屏补光**:全屏纯白覆盖层瞬间点亮,~300ms 后抓全分辨率帧(与预览同镜像策略 + 最长边 1280 JPEG),保持 ~300ms 再淡出(总 600–900ms),同步 `navigator.vibrate(30)`。
|
||||
- **手动兜底**:快门任何时刻可拍当前步(同样走白屏补光),不依赖检测;face-api/模型加载失败 → toast"自动识别不可用,已切换手动拍摄",全程手动。系统相机降级模式(getUserMedia 不可用/被拒)维持原样:无检测、无补光。
|
||||
- **相册选图**:相机页「相册」按钮可选当前步的图片(任何模式下可用),与实拍同管线(压 1280 → 推进流程)。
|
||||
- **语音播报**:步骤引导/姿态提示/倒计时/拍摄完成/分析中/失败共 14 条,文案与文件名契约见 `../docs/语音播报内容设计文档_V1.0.md`。WebAudio 单通道播放、同条 4s 去重;`assets/voice/manifest.json` 清单制,**语音文件待生成**——清单未提供时全部静默跳过。
|
||||
@@ -79,7 +81,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
- 报告底部:降级标注(`vm.notes` 非空才展示,如下颌线未参与评分)、综合分析(`vm.summary`)、生活方式建议(`vm.lifestyle`)、精华推荐卡(`vm.essence` + 触发判据);
|
||||
- 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。
|
||||
5. 真值回归:`node h5/test/regression.js` 以 `docs/皮肤检测测试结果_三视图_20260926.xlsx`(sheet「三视图检测汇总」,数据第 3/4 行=案例1/案例2)两组三视图真值对照评分引擎输出。
|
||||
6. **报告历史**:检测成功后按「检测」为单位存入 localStorage(`juzi.reports.v1`,上限 5 条,超配额逐条淘汰)——含三张照片、档案、全部评分数据与 marks 坐标,**不含 face_maps 整幅图层**(体积原因;回看时皱纹/肤色/敏感图降级为占位,光滑/痘痘标记叠加仍可用)。报告页导航栏「历史」进入列表,可回看/删除单条/清空。
|
||||
6. **报告历史**:检测成功后按「检测」为单位存入 localStorage(`juzi.reports.v1`,上限 5 条,超配额逐条淘汰)——含三张照片、档案、全部评分数据与 marks 坐标,**不含 face_maps 整幅图层**(体积原因;回看时皱纹/肤色/敏感图降级为占位,光滑/痘痘标记叠加仍可用)。当前无 UI 入口(第9轮移除),数据仅作本地追溯。
|
||||
7. **原始返回落盘**:服务端每次检测成功后把 Face++ 完整原始返回写入 `h5/data/results/<时间戳>-<request_id>.json`(复核/真值用,已 .gitignore;写盘失败不影响响应)。
|
||||
8. 检测失败按服务端 message 提示并退回档案页。
|
||||
|
||||
@@ -135,5 +137,5 @@ server {
|
||||
|
||||
- 尺寸换算:设计稿 1080px 基准 ÷ 2.88 = 375px 落地基准。
|
||||
- 图片性能:首屏背景 preload;首屏外 `loading="lazy"`。
|
||||
- 快门防连点/双流程令牌(camSeq/camSeqCam)防竞态;拍照页 15% 压暗遮罩 + 蒙版 58% 镂空压暗保证白字可读。
|
||||
- 快门防连点/双流程令牌(camSeq/camSeqCam)防竞态;拍照页取景框外纯黑(对齐 Figma),白字直接落在黑色上。
|
||||
- face-api.js 0.22.2 与模型全部本地化(`vendor/faceapi/`,约 1.4MB),运行时零外部请求,微信 webview / iOS Safari 弱网与内网部署可用;许可证(MIT / Apache-2.0)见 `vendor/faceapi/README.md`。
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,19 @@
|
||||
<svg width="112" height="30" viewBox="0 0 112 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4_299)">
|
||||
<mask id="mask0_4_299" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="112" height="30">
|
||||
<path d="M0 0H112V30H0V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4_299)">
|
||||
<path d="M104.35 24.9371C104.35 26.5464 103.046 27.8628 101.452 27.8628C99.8575 27.8628 98.5532 26.5464 98.5532 24.9371V5.27264C98.5532 3.66337 99.8575 2.34694 101.452 2.34694C103.046 2.34694 104.35 3.66337 104.35 5.27264V24.9371ZM108.987 24.0658V6.72785C108.987 3.59399 109.781 2.30905 111.875 2.50195V0.608173H91.0288V2.50195C93.1203 2.30905 93.9167 3.69701 93.9167 6.72785V24.0658C93.9167 27.0242 93.0786 28.1947 90.9032 27.9987V29.6016H112V27.9987C109.823 28.1947 108.987 27.093 108.987 24.0658Z" fill="#282828"/>
|
||||
<path d="M17.7768 21.0466C17.6508 22.5156 17.099 30 9.11663 30C0.937227 30 -6.10352e-05 22.7475 -6.10352e-05 14.9434C-6.10352e-05 5.54099 2.73023 0.00115776 9.42472 0.00115776C17.0523 0.00115776 17.6455 6.60578 17.763 8.92787H12.6678C12.5872 7.94213 12.5205 4.41107 9.3443 4.41107C6.11099 4.41107 5.32305 8.4062 5.32305 15.0524C5.32305 20.5271 5.86532 25.6672 9.20241 25.6672C12.2728 25.6672 12.5184 22.1605 12.6444 21.0466H17.7768Z" fill="#282828"/>
|
||||
<path d="M26.8478 14.6505C26.8478 21.5406 27.8825 25.6384 30.9022 25.6384C34.0805 25.6384 34.9879 21.8431 34.9879 14.6623C34.9879 7.91333 34.0231 4.40025 30.8978 4.40025C27.8178 4.40025 26.8478 8.18064 26.8478 14.6505ZM40.2697 14.7596C40.2697 25.3775 37.4101 30 30.7804 30C24.5845 30 21.5662 25.4459 21.5662 14.6998C21.5662 4.22301 25.0802 0.00019455 31.0142 0.00019455C37.2819 0.00019455 40.2697 4.67722 40.2697 14.7596Z" fill="#282828"/>
|
||||
<path d="M49.569 12.9081H51.9847C55.0943 12.9081 56.3102 11.5579 56.3102 8.62231C56.3102 6.12183 55.1261 4.67202 52.4898 4.67202H49.569V12.9081ZM49.569 17.1192V29.5H44.4462V0.462158H53.2555C58.4842 0.462158 61.4487 3.21487 61.4487 8.39559C61.4487 12.5948 59.2193 14.4068 57.7187 15.0078C59.946 16.0607 61.0009 17.9711 61.0009 22.2591V23.418C61.0009 26.7183 61.1544 27.9499 61.3886 29.4998H56.4065C55.986 28.171 55.9098 25.8715 55.9098 23.371V22.3126C55.9098 18.4319 55.0277 17.1192 51.7773 17.1192H49.569Z" fill="#282828"/>
|
||||
<path d="M81.5694 16.5932H72.3351V25.025H82.7026L82.0682 29.5H67.2516L67.2514 0.462158H82.0035L82.0037 5.01316H72.3351V12.0797H81.5694V16.5932Z" fill="#282828"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4_299">
|
||||
<rect width="112" height="30" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"files": [
|
||||
"step1-front",
|
||||
"step2-rightface",
|
||||
"step3-leftface",
|
||||
"step2-leftface",
|
||||
"step3-rightface",
|
||||
"hint-noface",
|
||||
"hint-near",
|
||||
"hint-far",
|
||||
|
||||
@@ -212,8 +212,8 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
}
|
||||
#s-cam.cam-live #cam-video { display: block; }
|
||||
#s-cam.cam-live .cam-bg { display: none; }
|
||||
/* 15% 黑遮罩,压住画面保证白色文案可读(静态示意图与实时画面共用) */
|
||||
#s-cam::before { content: ""; position: absolute; inset: 0; background: rgba(0,0,0,.15); z-index: 1; }
|
||||
/* 设计稿(Figma 05-07):取景框外全黑,仅轮廓镂空区显示实时画面,不再整屏压暗 */
|
||||
#s-cam::before { content: none; }
|
||||
#s-cam .nav { position: fixed; left: 0; right: 0; }
|
||||
.cam-step {
|
||||
position: absolute; left: 0; right: 0; top: 16.5%;
|
||||
@@ -266,7 +266,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
text-align: center; font-size: 8px; line-height: 10px; color: #fff;
|
||||
}
|
||||
|
||||
/* 取景蒙版:SVG mask 镂空压暗(58%) + 轮廓线框,按 data-step 切换轮廓 */
|
||||
/* 取景蒙版:SVG mask 镂空 + 轮廓线框,按 data-step 切换轮廓;框外纯黑不透明(对齐 Figma:只有轮廓内显示画面) */
|
||||
.cam-mask {
|
||||
position: absolute; inset: 0;
|
||||
width: 100%; height: 100%;
|
||||
@@ -276,7 +276,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
.cam-mask[data-step="0"] [data-shape="0"],
|
||||
.cam-mask[data-step="1"] [data-shape="1"],
|
||||
.cam-mask[data-step="2"] [data-shape="2"] { display: block; }
|
||||
.cam-dim-rect { fill: rgba(0,0,0,.58); }
|
||||
.cam-dim-rect { fill: #000; } /* 框外全黑(Figma 设计):摄像头画面只在轮廓镂空区可见 */
|
||||
.cam-line path { fill: none; stroke: rgba(255,255,255,.85); stroke-width: 2.5; }
|
||||
/* 未检测到人脸:轮廓呼吸动画(~1.8s);姿态 ok:品牌绿定格 */
|
||||
@keyframes maskBreath { 0%,100% { stroke-opacity: .3; } 50% { stroke-opacity: .95; } }
|
||||
@@ -331,88 +331,114 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
}
|
||||
.cam-nudge.show { opacity: 1; transform: translateX(-50%); }
|
||||
|
||||
/* ═══════════ 08 报告页 ═══════════ */
|
||||
.report-body { padding: 12px 38px 34px; }
|
||||
/* ═══════════ 08 报告页(Figma 2:9 严格还原:设计稿 1080 基准 ÷2.88) ═══════════ */
|
||||
.report-body { padding: 0 38px 20px; }
|
||||
/* 顶栏:白底通栏横带,左返回圆钮 + 居中黑标题(设计 255px 高通栏含手机状态栏,H5 只保留标题栏) */
|
||||
.report-nav {
|
||||
position: relative;
|
||||
margin: 0 -38px; /* 通栏:抵消 body 侧 padding */
|
||||
height: calc(44px + env(safe-area-inset-top));
|
||||
padding-top: env(safe-area-inset-top);
|
||||
background: #fff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.report-nav .nav-back {
|
||||
position: absolute; left: 8px;
|
||||
top: calc(50% + env(safe-area-inset-top) / 2); transform: translateY(-50%);
|
||||
}
|
||||
.report-nav-title { font-size: 14px; font-weight: 500; color: #000; letter-spacing: .5px; }
|
||||
.report-title {
|
||||
font-size: 20px; font-weight: 700; color: var(--ink);
|
||||
text-align: center; margin: 10px 0 16px;
|
||||
font-size: 20.6px; font-weight: 700; color: var(--ink);
|
||||
text-align: center; margin: 20px 0 13px;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
.card {
|
||||
background: #fff; border-radius: 6px;
|
||||
padding: 14px 15px;
|
||||
margin-bottom: 9px;
|
||||
background: #fff; border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/* 总分 */
|
||||
.card-stats { display: flex; padding: 11px 15px; }
|
||||
.stat { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
|
||||
.stat-label { font-size: 9px; font-weight: 500; color: var(--grey); }
|
||||
/* label 下短横线(34×1 居中) */
|
||||
/* 总分:两张独立白卡(设计 420+420,间距 17px→6px) */
|
||||
.stats-row { display: flex; gap: 6px; margin-bottom: 10px; }
|
||||
.stats-row .card-stat { flex: 1; margin-bottom: 0; }
|
||||
.card-stat {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 12px 10px 13px;
|
||||
}
|
||||
.stat-label { font-size: 8.7px; font-weight: 500; color: var(--grey); }
|
||||
/* label 下短横线(设计 99px→34px 居中) */
|
||||
.stat-label::after {
|
||||
content: ""; display: block;
|
||||
width: 34px; height: 1px; background: #EFEFEF;
|
||||
margin: 4px auto 0;
|
||||
}
|
||||
.stat-value { font-size: 29px; font-weight: 400; color: var(--ink); letter-spacing: 1px; line-height: 34px; }
|
||||
/* 维度(灰底独立文字,置于总分卡与维度卡之间) */
|
||||
.dims-intro { font-size: 10px; font-weight: 500; color: #464646; line-height: 1.6; margin: 0 2px 9px; }
|
||||
/* 大数字 30px/400 + 单位 15px/500(设计 87px 与 43.7px) */
|
||||
.stat-value { margin-top: 2px; line-height: 40px; color: var(--ink); }
|
||||
.stat-value b { font-size: 30px; font-weight: 400; letter-spacing: 1px; }
|
||||
.stat-value i { font-style: normal; font-size: 15px; font-weight: 500; }
|
||||
/* 三维度卡(设计:标签 8.7px 灰 / 分数 28.9px lh50 / 竖向短分隔线 21px / 最低分标红) */
|
||||
.card-dims { padding: 17px 19px 15px; }
|
||||
.dims-row { display: flex; }
|
||||
.dim { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
|
||||
.dim + .dim { border-left: 1px solid #EFEFEF; }
|
||||
.dim-name { font-size: 9px; font-weight: 500; color: var(--grey); }
|
||||
.dim-score { font-size: 29px; font-weight: 500; color: var(--ink); line-height: 1.1; }
|
||||
.dim-lv { font-size: 9px; font-weight: 500; min-height: 11px; }
|
||||
.dims-note { font-size: 9px; font-weight: 500; color: var(--grey); line-height: 1.55; margin-top: 9px; text-align: center; }
|
||||
.dims-disclaimer { font-size: 7px; font-weight: 400; color: var(--grey-3); margin: 0 2px 9px; line-height: 1.5; }
|
||||
/* 具体描述 */
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dtab {
|
||||
flex: 1; padding: 8px 0 9px;
|
||||
font-size: 10px; font-weight: 500; color: var(--ink);
|
||||
position: relative;
|
||||
.dim { flex: 1; position: relative; display: flex; flex-direction: column; align-items: center; }
|
||||
.dim + .dim::before {
|
||||
content: ""; position: absolute; left: 0; top: 37px;
|
||||
width: 1px; height: 21px; margin-top: -10.5px; background: #EFEFEF;
|
||||
}
|
||||
.dim-name { font-size: 8.7px; font-weight: 500; color: var(--grey); }
|
||||
.dim-score { font-size: 28.9px; font-weight: 500; color: var(--ink); line-height: 50px; }
|
||||
.dim-score.dim-bad { color: var(--bad); } /* 最低分标红 */
|
||||
/* 关注行(设计 fs25→8.7px:前缀灰 + 维度名深色大一号;下行深色口号) */
|
||||
.dims-note { margin-top: 2px; text-align: center; line-height: 1.5; }
|
||||
.dims-note-l1 { font-size: 8.7px; font-weight: 500; color: #858585; }
|
||||
.dims-note-l1 b { font-size: 10.1px; font-weight: 500; color: var(--ink); }
|
||||
.dims-note-l2 { font-size: 8.7px; font-weight: 500; color: var(--ink); }
|
||||
/* 具体描述(Tab 10.1px,选中 700 + 78×7 黄条;竖排条目标题在图左;图文垂直居中) */
|
||||
.card-detail { padding: 0 19px 19px; }
|
||||
.detail-tabs { display: flex; border-bottom: 1px solid #F0F0F0; }
|
||||
.dtab { flex: 1; padding: 13px 0 0; font-weight: 500; color: var(--ink); }
|
||||
.dtab-tx { position: relative; display: inline-block; font-size: 10.1px; padding: 0 5px 7px; }
|
||||
.dtab.active { font-weight: 700; }
|
||||
.dtab.active::after {
|
||||
content: ""; position: absolute;
|
||||
left: 50%; transform: translateX(-50%); bottom: -1px;
|
||||
width: 78px; height: 7px; border-radius: 3.5px;
|
||||
background: var(--yellow);
|
||||
.dtab.active .dtab-tx::after {
|
||||
content: ""; position: absolute; left: 50%; transform: translateX(-50%); bottom: -1px;
|
||||
width: 78px; height: 7px; border-radius: 3.5px; background: var(--yellow);
|
||||
}
|
||||
.dpanel { display: none; }
|
||||
.dpanel.active { display: block; animation: fade .25s ease; }
|
||||
.dpanel.active { display: block; animation: fade .25s ease; padding-top: 19px; }
|
||||
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
|
||||
.ditem { display: flex; gap: 11px; align-items: flex-start; }
|
||||
.ditem + .ditem { margin-top: 13px; padding-top: 13px; border-top: 1px solid #F0F0F0; }
|
||||
.ditem-img { width: 92px; height: 96px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
|
||||
.ditem-text { flex: 1; }
|
||||
.ditem { display: flex; gap: 9px; align-items: center; }
|
||||
.ditem + .ditem { margin-top: 18px; padding-top: 18px; border-top: 1px solid #F0F0F0; }
|
||||
.ditem-img { width: 92px; height: 96px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
||||
.ditem-text { flex: 1; min-width: 0; }
|
||||
/* 检测条目标题:竖排,位于图左(设计稿如此) */
|
||||
.ditem-title {
|
||||
writing-mode: vertical-rl;
|
||||
letter-spacing: 4px;
|
||||
font-size: 11.5px; font-weight: 500; color: var(--ink);
|
||||
margin-right: 12px; flex-shrink: 0;
|
||||
margin-right: 3.5px; flex-shrink: 0;
|
||||
}
|
||||
.ditem-para { font-size: 10.1px; font-weight: 400; color: var(--para); line-height: 1.41; overflow-wrap: anywhere; }
|
||||
/* 护理建议(标题 13px/700/#000;条目 10.1px #4A4A4A lh1.43;收尾句 11.1px/500/#000) */
|
||||
.card-care, .card-essence { padding: 18px 20px 16px 19px; }
|
||||
.card-heading { font-size: 13px; font-weight: 700; color: #000; margin-bottom: 4px; }
|
||||
.care-item { font-size: 10.1px; color: var(--para); line-height: 1.43; margin-bottom: 7px; overflow-wrap: anywhere; }
|
||||
.care-summary { font-size: 11.1px; font-weight: 500; color: #000; margin-top: 5px; }
|
||||
/* 精华推荐(Figma 无此卡,按业务文档 V2.3 §6.4 保留,样式沿用护理建议卡) */
|
||||
.essence-name { font-size: 12px; font-weight: 700; color: var(--ink); margin-bottom: 5px; }
|
||||
.essence-triggers { font-size: 7px; color: var(--grey-3); margin-top: 6px; }
|
||||
/* 产品推荐:大标题独立落在灰底;白卡左文案右图(图 151×174 撑满卡高,COREⅡ 为 Figma 导出字标 SVG) */
|
||||
.prod-title {
|
||||
font-size: 21.6px; font-weight: 700; color: var(--ink);
|
||||
line-height: 1.5; text-align: center; margin: 15px 0 16px;
|
||||
}
|
||||
.ditem-para { font-size: 10px; font-weight: 400; color: var(--para); line-height: 1.45; overflow-wrap: anywhere; }
|
||||
/* 护理建议 */
|
||||
.card-heading { font-size: 13px; font-weight: 700; color: #000; margin-bottom: 8px; }
|
||||
.care-item { font-size: 10px; color: var(--para); line-height: 1.6; margin-bottom: 8px; overflow-wrap: anywhere; }
|
||||
.care-summary { font-size: 11px; font-weight: 500; color: #000; margin-top: 10px; }
|
||||
/* 产品推荐:白底圆角卡,左侧文案 + 右侧单图(图高撑卡) */
|
||||
.prod-title { font-size: 21px; font-weight: 700; color: var(--ink); line-height: 1.45; text-align: center; margin: 4px 0 12px; }
|
||||
.card-product { padding: 0; overflow: hidden; }
|
||||
.prod-card { display: flex; align-items: stretch; }
|
||||
.prod-info {
|
||||
flex: 1; min-width: 0;
|
||||
padding: 18px 12px 18px 16px;
|
||||
display: flex; flex-direction: column; justify-content: center; gap: 7px;
|
||||
padding: 44px 10px 16px 16px;
|
||||
display: flex; flex-direction: column; align-items: flex-start;
|
||||
}
|
||||
.prod-badge { font-size: 8px; font-weight: 500; letter-spacing: 3px; color: #B9B9B9; }
|
||||
.prod-name { font-size: 13px; font-weight: 700; color: var(--ink); line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.prod-sub { font-size: 7.5px; font-weight: 500; color: var(--grey-3); }
|
||||
.prod-badge { width: 39px; height: auto; }
|
||||
.prod-name { font-size: 13px; font-weight: 700; color: var(--ink); line-height: 1.34; margin-top: 9px; overflow-wrap: anywhere; }
|
||||
.prod-sub { font-size: 7.3px; font-weight: 500; color: var(--grey-3); margin-top: auto; padding-top: 10px; }
|
||||
.prod-scroll {
|
||||
flex-shrink: 0; margin: 0; padding: 0;
|
||||
display: flex; overflow: hidden;
|
||||
@@ -420,15 +446,15 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
}
|
||||
.prod-scroll::-webkit-scrollbar { display: none; }
|
||||
.prod-scroll img {
|
||||
width: 168px; height: 225px;
|
||||
width: 151px; height: 174px;
|
||||
object-fit: cover; flex-shrink: 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
.btn-more { margin: 16px auto 0; }
|
||||
.btn-more { margin: 20px auto 0; }
|
||||
/* 免责小字:灰底独立文字,左对齐到卡片文案位(设计 x=166→57.6px;fw300 #000) */
|
||||
.report-disclaimer {
|
||||
font-size: 7px; font-weight: 300; color: var(--grey-3);
|
||||
line-height: 1.7; text-align: center;
|
||||
margin: 5px 2px 9px; /* 灰底独立文字,位于描述卡与护理建议卡之间 */
|
||||
font-size: 7.3px; font-weight: 300; color: #000;
|
||||
line-height: 1.44; text-align: left;
|
||||
margin: 13px 19px 11px;
|
||||
}
|
||||
|
||||
/* ═══════════ 弹窗 ═══════════ */
|
||||
@@ -636,23 +662,6 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
}
|
||||
.overlay-policy .policy-page { position: relative; }
|
||||
|
||||
/* #8 维度卡关注行:灰小字 + 深色口号(对齐设计稿) */
|
||||
.dims-note { margin-top: 12px; }
|
||||
.dims-note-l1 { font-size: 10px; font-weight: 400; color: var(--grey); }
|
||||
.dims-note-l2 { font-size: 11.5px; font-weight: 500; color: var(--ink); line-height: 1.8; }
|
||||
|
||||
/* #9 Tab 黄色指示条:宽度跟随文字(原固定 78px 过宽) */
|
||||
.dtab { padding: 8px 0 0; }
|
||||
.dtab.active::after { content: none; }
|
||||
.dtab-tx { position: relative; display: inline-block; padding: 0 5px 9px; }
|
||||
.dtab.active .dtab-tx::after {
|
||||
content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
|
||||
height: 7px; border-radius: 3.5px; background: var(--yellow);
|
||||
}
|
||||
|
||||
/* #13 分类卡图片对齐设计稿(约 100px 见方、圆角 10) */
|
||||
.ditem-img { width: 100px; height: 100px; border-radius: 10px; }
|
||||
|
||||
/* ═══════════ 第7轮:用户测试 agent 反馈 ═══════════ */
|
||||
/* P1-1 隐私页标题被悬浮返回键遮挡:正文上留返回键位 */
|
||||
#p-policy .policy-body { padding-top: 52px; }
|
||||
@@ -665,27 +674,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
.rs-list { z-index: 50; }
|
||||
/* #4 无进展提示再上移(取景/拍摄区下方,引导 chips 上方) */
|
||||
.cam-nudge { bottom: 24%; }
|
||||
/* #6 报告页顶栏移除,返回/历史改悬浮 */
|
||||
#s-report { position: relative; }
|
||||
.report-float-back {
|
||||
position: absolute; top: 12px; left: 12px; z-index: 21;
|
||||
background: #fff; box-shadow: 0 1px 6px rgba(0,0,0,.10);
|
||||
}
|
||||
.report-float-history {
|
||||
position: absolute; top: 12px; right: 12px; z-index: 21;
|
||||
font-size: 12px; color: var(--grey-2); padding: 7px 10px;
|
||||
background: #fff; border-radius: 15px; box-shadow: 0 1px 6px rgba(0,0,0,.10);
|
||||
}
|
||||
#s-report .report-body { padding-top: 44px; }
|
||||
/* #8 关注行:前缀灰 + 维度名深色 500 */
|
||||
.dims-note-l1 b { font-weight: 500; color: var(--ink); }
|
||||
/* #9 维度分:最低分标红(替代原档位徽章) */
|
||||
.dim-score.dim-bad { color: var(--bad); }
|
||||
/* #11 护理建议(设计稿样式:标题 + 编号条目) */
|
||||
#care-list .care-item { margin-bottom: 10px; }
|
||||
#care-list .care-item b { font-weight: 600; color: var(--ink); }
|
||||
/* 护理建议/精华推荐卡标题对齐设计稿(≈16px) */
|
||||
.card-care .card-heading, .card-essence .card-heading { font-size: 16px; }
|
||||
/* #6 报告页头部:按 Figma 08 还原为内联白底顶栏(见报告页主区块),不再悬浮 */
|
||||
|
||||
/* ═══════════ 第9轮:真机反馈 ═══════════ */
|
||||
/* #1 城市下拉浮层再提级,并加实体阴影边界(部分端 stacking 表现不同) */
|
||||
@@ -697,12 +686,6 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
font-size: 15px; font-weight: 500; text-shadow: 0 1px 8px rgba(0,0,0,.65);
|
||||
}
|
||||
.cam-hint.ok { background: none; color: #34C759; }
|
||||
/* #6 Tab 条对齐设计稿:文字 11px、选中 700,黄色指示条恢复设计宽度 78px */
|
||||
.dtab-tx { font-size: 11px; padding: 0 5px 10px; }
|
||||
.dtab.active .dtab-tx::after { left: 50%; right: auto; transform: translateX(-50%); width: 78px; }
|
||||
/* #7 关注行:维度名加粗 700,两行收紧 */
|
||||
.dims-note-l1 b { font-weight: 700; }
|
||||
.dims-note-l2 { line-height: 1.6; }
|
||||
/* #9 档案页顶栏移除,返回键悬浮(白圆带影,压暗背景上可读) */
|
||||
.profile-float-back {
|
||||
position: fixed; top: 12px; left: 12px; z-index: 21;
|
||||
|
||||
@@ -90,24 +90,25 @@
|
||||
<button class="nav-back nav-dark cam-back-float" data-action="cam-back" aria-label="返回"><i class="chev left"></i></button>
|
||||
<div class="cam-dots" id="cam-dots"><i></i><i></i><i></i></div>
|
||||
<p class="cam-step" id="cam-step">1/3<br>请单人正脸出镜</p>
|
||||
<!-- 取景蒙版:SVG mask 镂空压暗 + 轮廓线框;data-step 切换三种轮廓
|
||||
0=正脸(含耳部) 1=右脸出镜(头向左转,镜像画面中鼻朝左) 2=左脸出镜(头向右转,鼻朝右) -->
|
||||
<!-- 取景蒙版:SVG mask 镂空(框外全黑,仅轮廓内显示画面,对齐 Figma) + 轮廓线框;data-step 切换三种轮廓
|
||||
轮廓由 Figma 拍照示意图(design/assets/images/*-拍照示意.png,alpha 镂空区)逐像素描摹生成,等比缩放到 375×812 viewBox
|
||||
0=正脸(含耳部) 1=左脸出镜(头向右转,轮廓鼻朝画面左) 2=右脸出镜(头向左转,鼻朝画面右) -->
|
||||
<svg class="cam-mask" id="cam-mask" data-step="0" viewBox="0 0 375 812" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
|
||||
<defs>
|
||||
<mask id="cam-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="375" height="812">
|
||||
<rect x="0" y="0" width="375" height="812" fill="#fff"/>
|
||||
<g fill="#000">
|
||||
<path data-shape="0" d="M187.5 240 C252 240 300 285 302 365 C313 367 318 381 315 397 C312 412 305 418 299 420 C294 488 246 578 187.5 586 C129 578 81 488 76 420 C70 418 63 412 60 397 C57 381 62 367 73 365 C75 285 123 240 187.5 240 Z"/>
|
||||
<path data-shape="1" d="M170 245 C230 240 258 270 262 320 C263 330 262 338 259 345 C270 360 282 380 284 398 C285 405 282 411 274 413 C267 415 262 417 261 421 C267 425 268 431 263 435 C267 439 267 445 262 448 C264 454 263 460 265 466 C266 476 260 483 248 486 C220 505 185 530 158 548 C140 560 122 558 112 538 C95 500 90 430 94 370 C98 305 120 250 170 245 Z"/>
|
||||
<path data-shape="2" transform="translate(375 0) scale(-1 1)" d="M170 245 C230 240 258 270 262 320 C263 330 262 338 259 345 C270 360 282 380 284 398 C285 405 282 411 274 413 C267 415 262 417 261 421 C267 425 268 431 263 435 C267 439 267 445 262 448 C264 454 263 460 265 466 C266 476 260 483 248 486 C220 505 185 530 158 548 C140 560 122 558 112 538 C95 500 90 430 94 370 C98 305 120 250 170 245 Z"/>
|
||||
<path data-shape="0" d="M54.1 388.5L56.1 387.1L58.2 386.0L60.3 383.5L62.4 382.2L64.5 381.4L66.5 370.5L68.6 357.6L70.7 343.0L72.8 327.1L74.9 310.3L76.9 302.9L79.0 297.1L81.1 292.3L83.2 288.0L85.3 284.2L87.3 280.8L89.4 277.8L91.5 275.0L93.6 272.3L95.7 269.9L97.7 267.6L99.8 265.5L101.9 263.5L104.0 261.6L106.1 259.9L108.1 258.3L110.2 256.8L112.3 255.3L114.4 254.0L116.5 252.7L118.5 251.4L120.6 250.2L122.7 249.1L124.8 248.1L126.8 247.1L128.9 246.2L131.0 245.3L133.1 244.4L135.2 243.7L137.2 242.9L139.3 242.2L141.4 241.6L143.5 241.0L145.6 240.4L147.6 239.9L149.7 239.4L151.8 238.9L153.9 238.5L156.0 238.1L158.0 237.7L160.1 237.4L162.2 237.0L164.3 236.7L166.4 236.4L168.4 236.1L170.5 235.9L172.6 235.8L174.7 235.7L176.8 235.5L178.8 235.4L180.9 235.4L183.0 235.3L185.1 235.3L187.2 235.3L189.2 235.3L191.3 235.3L193.4 235.3L195.5 235.3L197.6 235.3L199.6 235.4L201.7 235.5L203.8 235.7L205.9 235.9L207.9 236.1L210.0 236.4L212.1 236.7L214.2 237.0L216.3 237.4L218.3 237.7L220.4 238.1L222.5 238.5L224.6 238.9L226.7 239.4L228.7 239.9L230.8 240.6L232.9 241.2L235.0 241.9L237.1 242.6L239.1 243.3L241.2 244.0L243.3 244.8L245.4 245.6L247.5 246.5L249.5 247.5L251.6 248.4L253.7 249.5L255.8 250.7L257.9 251.9L259.9 253.2L262.0 254.6L264.1 256.0L266.2 257.4L268.3 259.1L270.3 260.7L272.4 262.5L274.5 264.5L276.6 266.5L278.7 268.7L280.7 271.0L282.8 273.6L284.9 276.4L287.0 279.3L289.0 282.6L291.1 286.2L293.2 290.2L295.3 294.7L297.4 300.1L299.4 306.6L301.5 315.7L303.6 332.1L305.7 347.2L307.8 360.9L309.8 373.0L311.9 381.7L314.0 382.7L316.1 384.5L318.2 387.3L320.2 388.6L321.3 390.2L321.3 408.3L320.2 412.8L318.2 417.1L316.1 425.1L314.0 432.9L311.9 439.5L309.8 445.1L307.8 449.8L305.7 453.7L303.6 456.8L301.5 459.2L299.4 461.1L297.4 462.5L295.3 464.7L293.2 467.6L291.1 471.4L289.0 476.0L287.0 481.4L284.9 486.5L282.8 491.4L280.7 496.0L278.7 500.5L276.6 504.7L274.5 508.7L272.4 512.6L270.3 516.2L268.3 519.7L266.2 523.0L264.1 526.2L262.0 529.3L259.9 532.3L257.9 535.1L255.8 537.9L253.7 540.5L251.6 543.0L249.5 545.3L247.5 547.6L245.4 549.7L243.3 551.8L241.2 553.8L239.1 555.8L237.1 557.6L235.0 559.3L232.9 560.9L230.8 562.4L228.7 563.8L226.7 565.2L224.6 566.5L222.5 567.6L220.4 568.7L218.3 569.8L216.3 570.7L214.2 571.7L212.1 572.6L210.0 573.4L207.9 574.1L205.9 574.7L203.8 575.2L201.7 575.7L199.6 576.2L197.6 576.6L195.5 576.9L193.4 577.1L191.3 577.2L189.2 577.3L187.2 577.4L185.1 577.3L183.0 577.2L180.9 577.0L178.8 576.7L176.8 576.3L174.7 576.0L172.6 575.6L170.5 575.1L168.4 574.6L166.4 573.9L164.3 573.2L162.2 572.3L160.1 571.4L158.0 570.5L156.0 569.4L153.9 568.3L151.8 567.2L149.7 566.0L147.6 564.7L145.6 563.3L143.5 561.8L141.4 560.2L139.3 558.6L137.2 556.9L135.2 555.0L133.1 553.1L131.0 551.1L128.9 549.0L126.8 546.8L124.8 544.4L122.7 542.0L120.6 539.4L118.5 536.8L116.5 534.1L114.4 531.2L112.3 528.2L110.2 525.0L108.1 521.7L106.1 518.2L104.0 514.6L101.9 510.9L99.8 507.0L97.7 502.8L95.7 498.5L93.6 493.9L91.5 489.1L89.4 484.2L87.3 479.0L85.3 473.9L83.2 469.7L81.1 466.3L79.0 463.8L76.9 461.9L74.9 460.3L72.8 458.2L70.7 455.5L68.6 452.1L66.5 447.9L64.5 442.9L62.4 436.8L60.3 429.5L58.2 421.2L56.1 417.0L54.1 412.3Z"/>
|
||||
<path data-shape="1" d="M55.8 396.6L57.9 382.4L60.0 370.8L62.0 353.4L64.1 336.1L66.2 318.3L68.3 309.9L70.4 303.9L72.4 298.9L74.5 294.5L76.6 290.5L78.7 286.9L80.8 283.6L82.8 280.5L84.9 277.6L87.0 274.9L89.1 272.4L91.2 270.1L93.2 267.9L95.3 265.8L97.4 263.8L99.5 262.0L101.5 260.2L103.6 258.5L105.7 256.9L107.8 255.4L109.9 254.0L111.9 252.6L114.0 251.3L116.1 250.0L118.2 248.8L120.3 247.6L122.3 246.6L124.4 245.5L126.5 244.6L128.6 243.7L130.7 242.8L132.7 242.0L134.8 241.3L136.9 240.5L139.0 239.8L141.1 239.2L143.1 238.6L145.2 238.0L147.3 237.5L149.4 237.1L151.5 236.7L153.5 236.3L155.6 236.0L157.7 235.7L159.8 235.3L161.9 235.0L163.9 234.8L166.0 234.5L168.1 234.4L170.2 234.3L172.3 234.2L174.3 234.1L176.4 234.1L178.5 234.0L180.6 233.9L182.6 234.0L184.7 234.1L186.8 234.1L188.9 234.3L191.0 234.4L193.0 234.5L195.1 234.7L197.2 234.9L199.3 235.1L201.4 235.4L203.4 235.6L205.5 235.9L207.6 236.2L209.7 236.6L211.8 236.9L213.8 237.3L215.9 237.8L218.0 238.2L220.1 238.6L222.2 239.1L224.2 239.7L226.3 240.2L228.4 240.8L230.5 241.4L232.6 242.0L234.6 242.6L236.7 243.3L238.8 244.1L240.9 244.8L243.0 245.7L245.0 246.6L247.1 247.4L249.2 248.3L251.3 249.3L253.4 250.2L255.4 251.2L257.5 252.3L259.6 253.4L261.7 254.6L263.7 255.8L265.8 257.1L267.9 258.5L270.0 259.8L272.1 261.3L274.1 262.7L276.2 264.3L278.3 265.9L280.4 267.6L282.5 269.4L284.5 271.4L286.6 273.4L288.7 275.5L290.8 277.7L292.9 279.9L294.9 282.4L297.0 284.9L299.1 287.6L301.2 290.5L303.3 293.7L305.3 297.2L307.4 301.1L309.5 305.6L311.6 311.0L313.7 318.2L315.7 341.8L317.8 349.8L318.9 361.1L318.9 451.5L317.8 456.2L315.7 460.4L313.7 468.7L311.6 476.2L309.5 482.5L307.4 488.1L305.3 493.2L303.3 498.0L301.2 502.5L299.1 506.8L297.0 511.0L294.9 515.0L292.9 518.9L290.8 522.7L288.7 526.5L286.6 530.1L284.5 533.7L282.5 537.2L280.4 540.8L278.3 544.1L276.2 547.5L274.1 550.6L272.1 553.7L270.0 556.5L267.9 559.1L265.8 561.6L263.7 563.8L261.7 565.8L259.6 567.6L257.5 569.2L255.4 570.7L253.4 572.0L251.3 573.2L249.2 574.2L247.1 575.0L245.0 575.8L243.0 576.4L240.9 576.9L238.8 577.2L236.7 577.5L234.6 577.7L232.6 577.7L230.5 577.7L228.4 577.7L226.3 577.5L224.2 577.3L222.2 577.0L220.1 576.6L218.0 576.2L215.9 575.7L213.8 575.2L211.8 574.6L209.7 573.9L207.6 573.2L205.5 572.5L203.4 571.8L201.4 571.0L199.3 570.2L197.2 569.3L195.1 568.3L193.0 567.4L191.0 566.3L188.9 565.3L186.8 564.3L184.7 563.2L182.6 562.2L180.6 561.1L178.5 560.0L176.4 558.8L174.3 557.6L172.3 556.4L170.2 555.1L168.1 553.8L166.0 552.5L163.9 551.1L161.9 549.7L159.8 548.4L157.7 547.0L155.6 545.5L153.5 544.1L151.5 542.6L149.4 541.0L147.3 539.4L145.2 537.9L143.1 536.2L141.1 534.5L139.0 532.8L136.9 531.1L134.8 529.3L132.7 527.5L130.7 525.6L128.6 523.7L126.5 521.7L124.4 519.7L122.3 517.5L120.3 515.3L118.2 512.8L116.1 510.1L114.0 507.2L111.9 503.9L109.9 500.2L107.8 496.1L105.7 491.6L103.6 486.6L101.5 481.1L99.5 476.2L97.4 472.2L95.3 469.2L93.2 467.4L91.2 466.6L89.1 466.1L87.0 465.4L84.9 464.5L82.8 463.4L80.8 462.1L78.7 460.5L76.6 458.5L74.5 456.2L72.4 453.6L70.4 450.6L68.3 447.3L66.2 443.6L64.1 439.4L62.0 434.5L60.0 428.3L57.9 425.3L55.8 422.0Z"/>
|
||||
<path data-shape="2" d="M55.8 354.9L57.9 344.8L60.0 337.7L62.0 316.6L64.1 310.1L66.2 305.0L68.3 300.8L70.4 297.0L72.4 293.6L74.5 290.5L76.6 287.6L78.7 284.9L80.8 282.4L82.8 279.9L84.9 277.7L87.0 275.5L89.1 273.5L91.2 271.5L93.2 269.7L95.3 267.9L97.4 266.1L99.5 264.4L101.5 262.8L103.6 261.3L105.7 259.8L107.8 258.5L109.9 257.2L111.9 256.0L114.0 254.7L116.1 253.6L118.2 252.4L120.3 251.3L122.3 250.3L124.4 249.3L126.5 248.4L128.6 247.5L130.7 246.6L132.7 245.8L134.8 245.1L136.9 244.3L139.0 243.6L141.1 242.9L143.1 242.2L145.2 241.5L147.3 240.9L149.4 240.3L151.5 239.7L153.5 239.2L155.6 238.7L157.7 238.2L159.8 237.8L161.9 237.4L163.9 237.0L166.0 236.7L168.1 236.3L170.2 236.0L172.3 235.7L174.3 235.4L176.4 235.2L178.5 235.0L180.6 234.8L182.6 234.6L184.7 234.5L186.8 234.4L188.9 234.3L191.0 234.3L193.0 234.3L195.1 234.3L197.2 234.3L199.3 234.3L201.4 234.4L203.4 234.5L205.5 234.6L207.6 234.8L209.7 235.0L211.8 235.2L213.8 235.4L215.9 235.7L218.0 236.0L220.1 236.3L222.2 236.7L224.2 237.1L226.3 237.5L228.4 238.0L230.5 238.5L232.6 239.0L234.6 239.6L236.7 240.2L238.8 240.8L240.9 241.6L243.0 242.4L245.0 243.1L247.1 243.9L249.2 244.8L251.3 245.7L253.4 246.7L255.4 247.8L257.5 248.9L259.6 250.1L261.7 251.3L263.7 252.7L265.8 254.0L267.9 255.4L270.0 256.9L272.1 258.5L274.1 260.3L276.2 262.1L278.3 264.1L280.4 266.1L282.5 268.2L284.5 270.4L286.6 272.8L288.7 275.3L290.8 278.1L292.9 281.0L294.9 284.2L297.0 287.6L299.1 291.2L301.2 295.2L303.3 299.8L305.3 305.0L307.4 311.3L309.5 329.6L311.6 347.5L313.7 365.1L315.7 382.1L317.8 396.1L318.9 397.7L318.9 419.8L317.8 422.9L315.7 425.8L313.7 431.4L311.6 436.8L309.5 441.2L307.4 445.3L305.3 448.8L303.3 452.0L301.2 454.8L299.1 457.3L297.0 459.4L294.9 461.3L292.9 462.9L290.8 464.1L288.7 465.1L286.6 465.8L284.5 466.4L282.5 466.9L280.4 468.5L278.3 471.2L276.2 474.9L274.1 479.5L272.1 484.9L270.0 489.9L267.9 494.5L265.8 498.5L263.7 502.3L261.7 505.7L259.6 508.7L257.5 511.5L255.4 514.1L253.4 516.4L251.3 518.6L249.2 520.8L247.1 522.8L245.0 524.7L243.0 526.5L240.9 528.3L238.8 530.0L236.7 531.8L234.6 533.5L232.6 535.2L230.5 536.8L228.4 538.4L226.3 540.0L224.2 541.5L222.2 543.0L220.1 544.5L218.0 545.9L215.9 547.3L213.8 548.7L211.8 550.1L209.7 551.5L207.6 552.8L205.5 554.1L203.4 555.3L201.4 556.6L199.3 557.8L197.2 558.9L195.1 560.0L193.0 561.2L191.0 562.3L188.9 563.4L186.8 564.5L184.7 565.5L182.6 566.5L180.6 567.4L178.5 568.3L176.4 569.2L174.3 570.0L172.3 570.8L170.2 571.6L168.1 572.3L166.0 573.0L163.9 573.7L161.9 574.3L159.8 574.9L157.7 575.4L155.6 575.9L153.5 576.3L151.5 576.7L149.4 577.0L147.3 577.3L145.2 577.5L143.1 577.7L141.1 577.7L139.0 577.7L136.9 577.7L134.8 577.5L132.7 577.2L130.7 576.8L128.6 576.3L126.5 575.6L124.4 574.7L122.3 573.7L120.3 572.5L118.2 571.2L116.1 569.6L114.0 567.9L111.9 566.0L109.9 563.8L107.8 561.6L105.7 559.0L103.6 556.3L101.5 553.4L99.5 550.4L97.4 547.0L95.3 543.7L93.2 540.1L91.2 536.5L89.1 532.8L87.0 529.1L84.9 525.3L82.8 521.4L80.8 517.5L78.7 513.4L76.6 509.2L74.5 505.0L72.4 500.5L70.4 495.8L68.3 490.7L66.2 485.3L64.1 478.7L62.0 466.5L60.0 453.6L57.9 447.0L55.8 438.2Z"/>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect class="cam-dim-rect" x="0" y="0" width="375" height="812" mask="url(#cam-cut)"/>
|
||||
<g class="cam-line">
|
||||
<path data-shape="0" d="M187.5 240 C252 240 300 285 302 365 C313 367 318 381 315 397 C312 412 305 418 299 420 C294 488 246 578 187.5 586 C129 578 81 488 76 420 C70 418 63 412 60 397 C57 381 62 367 73 365 C75 285 123 240 187.5 240 Z"/>
|
||||
<path data-shape="1" d="M170 245 C230 240 258 270 262 320 C263 330 262 338 259 345 C270 360 282 380 284 398 C285 405 282 411 274 413 C267 415 262 417 261 421 C267 425 268 431 263 435 C267 439 267 445 262 448 C264 454 263 460 265 466 C266 476 260 483 248 486 C220 505 185 530 158 548 C140 560 122 558 112 538 C95 500 90 430 94 370 C98 305 120 250 170 245 Z"/>
|
||||
<path data-shape="2" transform="translate(375 0) scale(-1 1)" d="M170 245 C230 240 258 270 262 320 C263 330 262 338 259 345 C270 360 282 380 284 398 C285 405 282 411 274 413 C267 415 262 417 261 421 C267 425 268 431 263 435 C267 439 267 445 262 448 C264 454 263 460 265 466 C266 476 260 483 248 486 C220 505 185 530 158 548 C140 560 122 558 112 538 C95 500 90 430 94 370 C98 305 120 250 170 245 Z"/>
|
||||
<path data-shape="0" d="M54.1 388.5L56.1 387.1L58.2 386.0L60.3 383.5L62.4 382.2L64.5 381.4L66.5 370.5L68.6 357.6L70.7 343.0L72.8 327.1L74.9 310.3L76.9 302.9L79.0 297.1L81.1 292.3L83.2 288.0L85.3 284.2L87.3 280.8L89.4 277.8L91.5 275.0L93.6 272.3L95.7 269.9L97.7 267.6L99.8 265.5L101.9 263.5L104.0 261.6L106.1 259.9L108.1 258.3L110.2 256.8L112.3 255.3L114.4 254.0L116.5 252.7L118.5 251.4L120.6 250.2L122.7 249.1L124.8 248.1L126.8 247.1L128.9 246.2L131.0 245.3L133.1 244.4L135.2 243.7L137.2 242.9L139.3 242.2L141.4 241.6L143.5 241.0L145.6 240.4L147.6 239.9L149.7 239.4L151.8 238.9L153.9 238.5L156.0 238.1L158.0 237.7L160.1 237.4L162.2 237.0L164.3 236.7L166.4 236.4L168.4 236.1L170.5 235.9L172.6 235.8L174.7 235.7L176.8 235.5L178.8 235.4L180.9 235.4L183.0 235.3L185.1 235.3L187.2 235.3L189.2 235.3L191.3 235.3L193.4 235.3L195.5 235.3L197.6 235.3L199.6 235.4L201.7 235.5L203.8 235.7L205.9 235.9L207.9 236.1L210.0 236.4L212.1 236.7L214.2 237.0L216.3 237.4L218.3 237.7L220.4 238.1L222.5 238.5L224.6 238.9L226.7 239.4L228.7 239.9L230.8 240.6L232.9 241.2L235.0 241.9L237.1 242.6L239.1 243.3L241.2 244.0L243.3 244.8L245.4 245.6L247.5 246.5L249.5 247.5L251.6 248.4L253.7 249.5L255.8 250.7L257.9 251.9L259.9 253.2L262.0 254.6L264.1 256.0L266.2 257.4L268.3 259.1L270.3 260.7L272.4 262.5L274.5 264.5L276.6 266.5L278.7 268.7L280.7 271.0L282.8 273.6L284.9 276.4L287.0 279.3L289.0 282.6L291.1 286.2L293.2 290.2L295.3 294.7L297.4 300.1L299.4 306.6L301.5 315.7L303.6 332.1L305.7 347.2L307.8 360.9L309.8 373.0L311.9 381.7L314.0 382.7L316.1 384.5L318.2 387.3L320.2 388.6L321.3 390.2L321.3 408.3L320.2 412.8L318.2 417.1L316.1 425.1L314.0 432.9L311.9 439.5L309.8 445.1L307.8 449.8L305.7 453.7L303.6 456.8L301.5 459.2L299.4 461.1L297.4 462.5L295.3 464.7L293.2 467.6L291.1 471.4L289.0 476.0L287.0 481.4L284.9 486.5L282.8 491.4L280.7 496.0L278.7 500.5L276.6 504.7L274.5 508.7L272.4 512.6L270.3 516.2L268.3 519.7L266.2 523.0L264.1 526.2L262.0 529.3L259.9 532.3L257.9 535.1L255.8 537.9L253.7 540.5L251.6 543.0L249.5 545.3L247.5 547.6L245.4 549.7L243.3 551.8L241.2 553.8L239.1 555.8L237.1 557.6L235.0 559.3L232.9 560.9L230.8 562.4L228.7 563.8L226.7 565.2L224.6 566.5L222.5 567.6L220.4 568.7L218.3 569.8L216.3 570.7L214.2 571.7L212.1 572.6L210.0 573.4L207.9 574.1L205.9 574.7L203.8 575.2L201.7 575.7L199.6 576.2L197.6 576.6L195.5 576.9L193.4 577.1L191.3 577.2L189.2 577.3L187.2 577.4L185.1 577.3L183.0 577.2L180.9 577.0L178.8 576.7L176.8 576.3L174.7 576.0L172.6 575.6L170.5 575.1L168.4 574.6L166.4 573.9L164.3 573.2L162.2 572.3L160.1 571.4L158.0 570.5L156.0 569.4L153.9 568.3L151.8 567.2L149.7 566.0L147.6 564.7L145.6 563.3L143.5 561.8L141.4 560.2L139.3 558.6L137.2 556.9L135.2 555.0L133.1 553.1L131.0 551.1L128.9 549.0L126.8 546.8L124.8 544.4L122.7 542.0L120.6 539.4L118.5 536.8L116.5 534.1L114.4 531.2L112.3 528.2L110.2 525.0L108.1 521.7L106.1 518.2L104.0 514.6L101.9 510.9L99.8 507.0L97.7 502.8L95.7 498.5L93.6 493.9L91.5 489.1L89.4 484.2L87.3 479.0L85.3 473.9L83.2 469.7L81.1 466.3L79.0 463.8L76.9 461.9L74.9 460.3L72.8 458.2L70.7 455.5L68.6 452.1L66.5 447.9L64.5 442.9L62.4 436.8L60.3 429.5L58.2 421.2L56.1 417.0L54.1 412.3Z"/>
|
||||
<path data-shape="1" d="M55.8 396.6L57.9 382.4L60.0 370.8L62.0 353.4L64.1 336.1L66.2 318.3L68.3 309.9L70.4 303.9L72.4 298.9L74.5 294.5L76.6 290.5L78.7 286.9L80.8 283.6L82.8 280.5L84.9 277.6L87.0 274.9L89.1 272.4L91.2 270.1L93.2 267.9L95.3 265.8L97.4 263.8L99.5 262.0L101.5 260.2L103.6 258.5L105.7 256.9L107.8 255.4L109.9 254.0L111.9 252.6L114.0 251.3L116.1 250.0L118.2 248.8L120.3 247.6L122.3 246.6L124.4 245.5L126.5 244.6L128.6 243.7L130.7 242.8L132.7 242.0L134.8 241.3L136.9 240.5L139.0 239.8L141.1 239.2L143.1 238.6L145.2 238.0L147.3 237.5L149.4 237.1L151.5 236.7L153.5 236.3L155.6 236.0L157.7 235.7L159.8 235.3L161.9 235.0L163.9 234.8L166.0 234.5L168.1 234.4L170.2 234.3L172.3 234.2L174.3 234.1L176.4 234.1L178.5 234.0L180.6 233.9L182.6 234.0L184.7 234.1L186.8 234.1L188.9 234.3L191.0 234.4L193.0 234.5L195.1 234.7L197.2 234.9L199.3 235.1L201.4 235.4L203.4 235.6L205.5 235.9L207.6 236.2L209.7 236.6L211.8 236.9L213.8 237.3L215.9 237.8L218.0 238.2L220.1 238.6L222.2 239.1L224.2 239.7L226.3 240.2L228.4 240.8L230.5 241.4L232.6 242.0L234.6 242.6L236.7 243.3L238.8 244.1L240.9 244.8L243.0 245.7L245.0 246.6L247.1 247.4L249.2 248.3L251.3 249.3L253.4 250.2L255.4 251.2L257.5 252.3L259.6 253.4L261.7 254.6L263.7 255.8L265.8 257.1L267.9 258.5L270.0 259.8L272.1 261.3L274.1 262.7L276.2 264.3L278.3 265.9L280.4 267.6L282.5 269.4L284.5 271.4L286.6 273.4L288.7 275.5L290.8 277.7L292.9 279.9L294.9 282.4L297.0 284.9L299.1 287.6L301.2 290.5L303.3 293.7L305.3 297.2L307.4 301.1L309.5 305.6L311.6 311.0L313.7 318.2L315.7 341.8L317.8 349.8L318.9 361.1L318.9 451.5L317.8 456.2L315.7 460.4L313.7 468.7L311.6 476.2L309.5 482.5L307.4 488.1L305.3 493.2L303.3 498.0L301.2 502.5L299.1 506.8L297.0 511.0L294.9 515.0L292.9 518.9L290.8 522.7L288.7 526.5L286.6 530.1L284.5 533.7L282.5 537.2L280.4 540.8L278.3 544.1L276.2 547.5L274.1 550.6L272.1 553.7L270.0 556.5L267.9 559.1L265.8 561.6L263.7 563.8L261.7 565.8L259.6 567.6L257.5 569.2L255.4 570.7L253.4 572.0L251.3 573.2L249.2 574.2L247.1 575.0L245.0 575.8L243.0 576.4L240.9 576.9L238.8 577.2L236.7 577.5L234.6 577.7L232.6 577.7L230.5 577.7L228.4 577.7L226.3 577.5L224.2 577.3L222.2 577.0L220.1 576.6L218.0 576.2L215.9 575.7L213.8 575.2L211.8 574.6L209.7 573.9L207.6 573.2L205.5 572.5L203.4 571.8L201.4 571.0L199.3 570.2L197.2 569.3L195.1 568.3L193.0 567.4L191.0 566.3L188.9 565.3L186.8 564.3L184.7 563.2L182.6 562.2L180.6 561.1L178.5 560.0L176.4 558.8L174.3 557.6L172.3 556.4L170.2 555.1L168.1 553.8L166.0 552.5L163.9 551.1L161.9 549.7L159.8 548.4L157.7 547.0L155.6 545.5L153.5 544.1L151.5 542.6L149.4 541.0L147.3 539.4L145.2 537.9L143.1 536.2L141.1 534.5L139.0 532.8L136.9 531.1L134.8 529.3L132.7 527.5L130.7 525.6L128.6 523.7L126.5 521.7L124.4 519.7L122.3 517.5L120.3 515.3L118.2 512.8L116.1 510.1L114.0 507.2L111.9 503.9L109.9 500.2L107.8 496.1L105.7 491.6L103.6 486.6L101.5 481.1L99.5 476.2L97.4 472.2L95.3 469.2L93.2 467.4L91.2 466.6L89.1 466.1L87.0 465.4L84.9 464.5L82.8 463.4L80.8 462.1L78.7 460.5L76.6 458.5L74.5 456.2L72.4 453.6L70.4 450.6L68.3 447.3L66.2 443.6L64.1 439.4L62.0 434.5L60.0 428.3L57.9 425.3L55.8 422.0Z"/>
|
||||
<path data-shape="2" d="M55.8 354.9L57.9 344.8L60.0 337.7L62.0 316.6L64.1 310.1L66.2 305.0L68.3 300.8L70.4 297.0L72.4 293.6L74.5 290.5L76.6 287.6L78.7 284.9L80.8 282.4L82.8 279.9L84.9 277.7L87.0 275.5L89.1 273.5L91.2 271.5L93.2 269.7L95.3 267.9L97.4 266.1L99.5 264.4L101.5 262.8L103.6 261.3L105.7 259.8L107.8 258.5L109.9 257.2L111.9 256.0L114.0 254.7L116.1 253.6L118.2 252.4L120.3 251.3L122.3 250.3L124.4 249.3L126.5 248.4L128.6 247.5L130.7 246.6L132.7 245.8L134.8 245.1L136.9 244.3L139.0 243.6L141.1 242.9L143.1 242.2L145.2 241.5L147.3 240.9L149.4 240.3L151.5 239.7L153.5 239.2L155.6 238.7L157.7 238.2L159.8 237.8L161.9 237.4L163.9 237.0L166.0 236.7L168.1 236.3L170.2 236.0L172.3 235.7L174.3 235.4L176.4 235.2L178.5 235.0L180.6 234.8L182.6 234.6L184.7 234.5L186.8 234.4L188.9 234.3L191.0 234.3L193.0 234.3L195.1 234.3L197.2 234.3L199.3 234.3L201.4 234.4L203.4 234.5L205.5 234.6L207.6 234.8L209.7 235.0L211.8 235.2L213.8 235.4L215.9 235.7L218.0 236.0L220.1 236.3L222.2 236.7L224.2 237.1L226.3 237.5L228.4 238.0L230.5 238.5L232.6 239.0L234.6 239.6L236.7 240.2L238.8 240.8L240.9 241.6L243.0 242.4L245.0 243.1L247.1 243.9L249.2 244.8L251.3 245.7L253.4 246.7L255.4 247.8L257.5 248.9L259.6 250.1L261.7 251.3L263.7 252.7L265.8 254.0L267.9 255.4L270.0 256.9L272.1 258.5L274.1 260.3L276.2 262.1L278.3 264.1L280.4 266.1L282.5 268.2L284.5 270.4L286.6 272.8L288.7 275.3L290.8 278.1L292.9 281.0L294.9 284.2L297.0 287.6L299.1 291.2L301.2 295.2L303.3 299.8L305.3 305.0L307.4 311.3L309.5 329.6L311.6 347.5L313.7 365.1L315.7 382.1L317.8 396.1L318.9 397.7L318.9 419.8L317.8 422.9L315.7 425.8L313.7 431.4L311.6 436.8L309.5 441.2L307.4 445.3L305.3 448.8L303.3 452.0L301.2 454.8L299.1 457.3L297.0 459.4L294.9 461.3L292.9 462.9L290.8 464.1L288.7 465.1L286.6 465.8L284.5 466.4L282.5 466.9L280.4 468.5L278.3 471.2L276.2 474.9L274.1 479.5L272.1 484.9L270.0 489.9L267.9 494.5L265.8 498.5L263.7 502.3L261.7 505.7L259.6 508.7L257.5 511.5L255.4 514.1L253.4 516.4L251.3 518.6L249.2 520.8L247.1 522.8L245.0 524.7L243.0 526.5L240.9 528.3L238.8 530.0L236.7 531.8L234.6 533.5L232.6 535.2L230.5 536.8L228.4 538.4L226.3 540.0L224.2 541.5L222.2 543.0L220.1 544.5L218.0 545.9L215.9 547.3L213.8 548.7L211.8 550.1L209.7 551.5L207.6 552.8L205.5 554.1L203.4 555.3L201.4 556.6L199.3 557.8L197.2 558.9L195.1 560.0L193.0 561.2L191.0 562.3L188.9 563.4L186.8 564.5L184.7 565.5L182.6 566.5L180.6 567.4L178.5 568.3L176.4 569.2L174.3 570.0L172.3 570.8L170.2 571.6L168.1 572.3L166.0 573.0L163.9 573.7L161.9 574.3L159.8 574.9L157.7 575.4L155.6 575.9L153.5 576.3L151.5 576.7L149.4 577.0L147.3 577.3L145.2 577.5L143.1 577.7L141.1 577.7L139.0 577.7L136.9 577.7L134.8 577.5L132.7 577.2L130.7 576.8L128.6 576.3L126.5 575.6L124.4 574.7L122.3 573.7L120.3 572.5L118.2 571.2L116.1 569.6L114.0 567.9L111.9 566.0L109.9 563.8L107.8 561.6L105.7 559.0L103.6 556.3L101.5 553.4L99.5 550.4L97.4 547.0L95.3 543.7L93.2 540.1L91.2 536.5L89.1 532.8L87.0 529.1L84.9 525.3L82.8 521.4L80.8 517.5L78.7 513.4L76.6 509.2L74.5 505.0L72.4 500.5L70.4 495.8L68.3 490.7L66.2 485.3L64.1 478.7L62.0 466.5L60.0 453.6L57.9 447.0L55.8 438.2Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="cam-status" id="cam-status">检测引擎加载中…</p>
|
||||
@@ -134,18 +135,23 @@
|
||||
|
||||
<!-- ═══════════ 08 报告页 ═══════════ -->
|
||||
<section class="screen" id="s-report">
|
||||
<button class="nav-back report-float-back" data-action="back-profile" aria-label="返回"><i class="chev left"></i></button>
|
||||
<div class="report-body">
|
||||
<!-- 顶栏(Figma 08:白底横带,左返回圆钮 + 居中标题) -->
|
||||
<header class="report-nav">
|
||||
<button class="nav-back" data-action="back-profile" aria-label="返回"><i class="chev left"></i></button>
|
||||
<span class="report-nav-title">1'me在线肌肤测试</span>
|
||||
</header>
|
||||
<h1 class="report-title">您的肌肤状态表现</h1>
|
||||
|
||||
<div class="card card-stats">
|
||||
<div class="stat">
|
||||
<!-- 总分:Figma 为两张独立白卡(综合评分 / 肌肤年龄) -->
|
||||
<div class="stats-row">
|
||||
<div class="card card-stat">
|
||||
<span class="stat-label">综合评分</span>
|
||||
<span class="stat-value" id="stat-score">--分</span>
|
||||
<span class="stat-value" id="stat-score"><b>--</b><i>分</i></span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="card card-stat">
|
||||
<span class="stat-label">肌肤年龄</span>
|
||||
<span class="stat-value" id="stat-age">--岁</span>
|
||||
<span class="stat-value" id="stat-age"><b>--</b><i>岁</i></span>
|
||||
<span class="stat-sub" id="stat-age-note"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,6 +232,7 @@
|
||||
<div id="care-list">
|
||||
<p class="care-item">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
<p class="care-summary">好状态,是内外同调的长期节奏。</p>
|
||||
</div>
|
||||
|
||||
<div class="card card-essence">
|
||||
@@ -235,11 +242,12 @@
|
||||
<p class="essence-triggers" id="essence-triggers"></p>
|
||||
</div>
|
||||
|
||||
<!-- 产品推荐大标题独立于白卡之外,直接落在灰底上(Figma 08) -->
|
||||
<h2 class="prod-title">感受高光时刻<br>从日常的好状态开始</h2>
|
||||
<div class="card card-product">
|
||||
<h2 class="prod-title">感受高光时刻<br>从日常的好状态开始</h2>
|
||||
<div class="prod-card">
|
||||
<div class="prod-info">
|
||||
<span class="prod-badge">CORE Ⅱ</span>
|
||||
<img class="prod-badge" src="assets/svg/prod-badge.svg" alt="CORE Ⅱ">
|
||||
<h3 class="prod-name">以我自在有势挺括双仓精华液</h3>
|
||||
<p class="prod-sub">参透皮下根源 28天校准皮肤状态</p>
|
||||
</div>
|
||||
|
||||
@@ -168,13 +168,16 @@
|
||||
var IS_MOCK = /[?&]mock=1/.test(location.search); // mock:不加载 face-api、不跑检测,analyze 直返内置样本
|
||||
var camVideo = $('#cam-video'), camFile = $('#cam-file'), albumFile = $('#album-file');
|
||||
var camStream = null, camLive = false, camFallback = false, camSeqCam = 0; // camSeqCam:相机启动令牌,防止迟到的授权回调泄漏摄像头
|
||||
var photos = []; // 三连拍(dataURL),与 CAM_STEPS 同序:[0]正脸 [1]右脸(头左转) [2]左脸(头右转)
|
||||
var photos = []; // 三连拍(dataURL),与 CAM_STEPS 同序:[0]正脸 [1]左脸(头右转) [2]右脸(头左转)
|
||||
|
||||
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
|
||||
var camFacing = 'user'; // 当前镜头:'user' 前置 | 'environment' 后置(翻转按钮切换)
|
||||
var camMirrored = true; // 预览是否镜像(前置镜像,后置不镜像)
|
||||
var mirrorCalibrated = false; // 镜像自校准:本轮是否已翻转过一次(防振荡,一次校准管整个会话)
|
||||
var camMirrored = true; // 前置镜像开关:仅正脸步生效(自拍习惯);侧脸步始终不镜像(左右不再迷惑,与取景轮廓方向一致)
|
||||
var sideYawFlip = false; // 侧脸步语义 yaw 反向标志:个别机型前置帧本身已镜像时由自校准翻转
|
||||
var mirrorCalibrated = false; // 侧脸自校准:本轮是否已翻转过一次(防振荡,一次校准管整个会话)
|
||||
var oppFrames = 0; // 侧脸步语义 yaw 连续反向帧计数
|
||||
// 预览/抓帧是否镜像:正脸步跟随 camMirrored,侧脸步恒不镜像
|
||||
function effMirror() { return camMirrored && CAM_STEPS[camStep].target === 'front'; }
|
||||
function getCamStream() {
|
||||
return navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: camFacing }, width: { ideal: 1280 }, height: { ideal: 1280 } },
|
||||
@@ -207,7 +210,7 @@
|
||||
if (token !== camSeqCam) { stream.getTracks().forEach(function (t) { t.stop(); }); return; } // 期间已退出拍照页:立即释放
|
||||
camStream = stream;
|
||||
camVideo.srcObject = stream;
|
||||
camVideo.classList.toggle('mirror', camMirrored);
|
||||
camVideo.classList.toggle('mirror', effMirror());
|
||||
// iOS 来电/切后台等系统打断会结束 track:挂 ended 监听走自动重连
|
||||
stream.getTracks().forEach(function (t) {
|
||||
t.onended = function () { onCamTrackEnded(token); };
|
||||
@@ -247,22 +250,18 @@
|
||||
resetDetectState(); // 降级无检测、无补光:蒙版回到静态线框
|
||||
toast('已切换为系统相机拍摄');
|
||||
}
|
||||
// 从视频帧截取照片(镜像绘制,与预览一致;压到最长边 1280 的 JPEG)
|
||||
// 从视频帧截取照片(完整帧,不镜像侧脸步;压到最长边 1280 的 JPEG)
|
||||
// 抓完整帧而非取景框可见区:新 UI 轮廓外全黑,用户只关注轮廓内人脸;按可见区裁切在侧脸/偏站位时会切掉人脸(Face++ INVALID_IMAGE_FACE)
|
||||
function captureFromVideo() {
|
||||
var vw = camVideo.videoWidth, vh = camVideo.videoHeight;
|
||||
if (!vw || !vh) return null;
|
||||
// 只取取景框可见区域(object-fit:cover 的中心裁切):照片 = 用户当时看到的画面
|
||||
var box = camVideo.getBoundingClientRect();
|
||||
var scaleFit = Math.max(box.width / vw, box.height / vh);
|
||||
var visW = Math.min(vw, box.width / scaleFit), visH = Math.min(vh, box.height / scaleFit);
|
||||
var sx = (vw - visW) / 2, sy = (vh - visH) / 2;
|
||||
var scale = Math.min(1, 1280 / Math.max(visW, visH));
|
||||
var cw = Math.round(visW * scale), ch = Math.round(visH * scale);
|
||||
var scale = Math.min(1, 1280 / Math.max(vw, vh));
|
||||
var cw = Math.round(vw * scale), ch = Math.round(vh * scale);
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = cw; canvas.height = ch;
|
||||
var ctx = canvas.getContext('2d');
|
||||
if (camMirrored) { ctx.translate(cw, 0); ctx.scale(-1, 1); } // 前置镜像,与预览一致
|
||||
ctx.drawImage(camVideo, sx, sy, visW, visH, 0, 0, cw, ch);
|
||||
if (effMirror()) { ctx.translate(cw, 0); ctx.scale(-1, 1); } // 与预览一致:正脸步前置镜像,侧脸步不镜像(照片=摄像头原始视角)
|
||||
ctx.drawImage(camVideo, 0, 0, vw, vh, 0, 0, cw, ch);
|
||||
return canvas.toDataURL('image/jpeg', 0.88);
|
||||
}
|
||||
// 文件 → 统一压成最长边 1280 的 JPEG → 推进流程(降级系统相机与相册选图共用)
|
||||
@@ -303,7 +302,7 @@
|
||||
/* ─── 语音播报(WebAudio 单通道;assets/voice/<id>.mp3,缺失静默跳过;同条 4s 去重)
|
||||
文案/文件名契约:docs/语音播报内容设计文档_V1.0.md ─── */
|
||||
var VOICE = {
|
||||
step: ['step1-front', 'step2-rightface', 'step3-leftface'],
|
||||
step: ['step1-front', 'step2-leftface', 'step3-rightface'],
|
||||
hint: {
|
||||
'未检测到人脸': 'hint-noface', '请靠近一点': 'hint-near', '请远一点': 'hint-far',
|
||||
'请正对屏幕': 'hint-front', '请向左转头': 'hint-turn-left', '请向右转头': 'hint-turn-right',
|
||||
@@ -312,7 +311,7 @@
|
||||
countdown: 'countdown-321', shot: 'shot-done', analyzing: 'analyzing', fail: 'analyze-fail'
|
||||
};
|
||||
var voice = (function () {
|
||||
var ctx = null, lastAt = {}, cur = null, files = null; // files:null=清单未加载;{}=无可用语音
|
||||
var ctx = null, lastAt = {}, cur = null, curEnd = 0, files = null; // files:null=清单未加载;{}=无可用语音
|
||||
// 清单制:只播 assets/voice/manifest.json 里声明的文件,不逐个探文件(避免控制台 404 噪音)
|
||||
fetch('assets/voice/manifest.json')
|
||||
.then(function (r) { return r.ok ? r.json() : { files: [] }; })
|
||||
@@ -333,15 +332,29 @@
|
||||
if (!ab || !ctx) return;
|
||||
ctx.decodeAudioData(ab, function (buf) {
|
||||
if (cur) { try { cur.stop(); } catch (e) {} }
|
||||
cur = ctx.createBufferSource();
|
||||
cur.buffer = buf;
|
||||
cur.connect(ctx.destination);
|
||||
cur.start(0);
|
||||
var src = ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
src.connect(ctx.destination);
|
||||
cur = src;
|
||||
curEnd = Date.now() + buf.duration * 1000;
|
||||
src.onended = function () { if (cur === src) cur = null; };
|
||||
src.start(0);
|
||||
}, function () { /* 解码失败忽略 */ });
|
||||
})
|
||||
.catch(function () { /* 网络异常忽略 */ });
|
||||
}
|
||||
return { unlock: unlock, play: play };
|
||||
// 等当前播报自然收尾后执行 cb(倒计时接续「保持不动」,不拦腰切断);无播报在执行则立即执行
|
||||
// 返回取消函数;注意:cur 被新播报 stop 也会触发 onended,调用方需配合 cancelCountdown 等路径取消
|
||||
function afterCurrent(cb) {
|
||||
if (!cur || Date.now() >= curEnd - 60) { cb(); return null; }
|
||||
var src = cur, cancelled = false;
|
||||
src.onended = function () {
|
||||
if (cur === src) cur = null;
|
||||
if (!cancelled) cb();
|
||||
};
|
||||
return function () { cancelled = true; };
|
||||
}
|
||||
return { unlock: unlock, play: play, afterCurrent: afterCurrent };
|
||||
})();
|
||||
document.addEventListener('click', voice.unlock); // 任意点击即完成解锁
|
||||
|
||||
@@ -360,6 +373,7 @@
|
||||
var detRunning = false, detTimer = null; // 检测循环
|
||||
var okSince = 0; // 当前连续 ok 起点(0=未在累积)
|
||||
var counting = false, cdTimer = null, cdN = 0; // 倒计时
|
||||
var cdDeferCancel = null; // 倒计时等播报收尾的取消句柄(afterCurrent)
|
||||
var nudgeTimer = null; // 12s 无进展提示
|
||||
var capturing = false; // 白屏补光抓帧中(暂停姿态判定)
|
||||
var lastHint = ''; // 提示文案去重
|
||||
@@ -419,15 +433,21 @@
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
counting = true;
|
||||
counting = true; // 立即置位:等待播报收尾期间姿态被破坏,走 cancelCountdown 统一取消
|
||||
clearTimeout(nudgeTimer);
|
||||
hideNudge();
|
||||
setMask('ok'); // 轮廓保持绿色
|
||||
setHint('保持不动', true, /* silent= */true); // 倒计时前不再抢播「保持不动」(会被 321 立刻切断,听着像吞字)
|
||||
// 「保持不动」等仍在播放的提示音自然播完再进倒计时,画面数字与「三、二、一」同步起播
|
||||
cdDeferCancel = voice.afterCurrent(beginCountdown);
|
||||
}
|
||||
function beginCountdown() {
|
||||
cdDeferCancel = null;
|
||||
if (!counting) return; // 等待期间已被取消(姿态破坏/退出拍照页)
|
||||
cdN = COUNTDOWN_S;
|
||||
var c = $('#cam-count');
|
||||
c.textContent = cdN;
|
||||
c.classList.add('show');
|
||||
setMask('ok'); // 轮廓保持绿色
|
||||
setHint('保持不动', true, /* silent= */true); // 倒计时前不再抢播「保持不动」(会被 321 立刻切断,听着像吞字)
|
||||
voice.play(VOICE.countdown); // 「三、二、一」与数字同步起播
|
||||
cdTimer = setTimeout(cdTick, 1000);
|
||||
}
|
||||
@@ -444,6 +464,7 @@
|
||||
}
|
||||
function cancelCountdown() {
|
||||
counting = false;
|
||||
if (cdDeferCancel) { cdDeferCancel(); cdDeferCancel = null; } // 取消尚未起播的倒计时接续
|
||||
clearTimeout(cdTimer);
|
||||
$('#cam-count').classList.remove('show');
|
||||
}
|
||||
@@ -495,19 +516,20 @@
|
||||
if (ratio < FACE_NEAR) hint = '请靠近一点';
|
||||
else if (ratio > FACE_FAR) hint = '请远一点';
|
||||
else {
|
||||
// 镜像标志跟随镜头(前置 true/后置 false);距离不合格时角度不判 ok
|
||||
var pose = HeadPose.check(d.landmarks.positions, CAM_STEPS[camStep].target, camMirrored);
|
||||
// 镜像自校准:侧脸步用户实际转向与语义 yaw 持续相反(本机镜像策略与 camMirrored 假设不符),
|
||||
// 连续 5 帧强反向即翻转镜像约定一次(翻转后语义回正,通常立即姿态合格)
|
||||
// 距离不合格时角度不判 ok。镜像参数:正脸步跟随镜头(前置 true/后置 false);
|
||||
// 侧脸步预览不镜像,特征点即原始视角,标准机型恒为 false,个别前置帧已镜像的机型由自校准翻转 sideYawFlip
|
||||
var tgt = CAM_STEPS[camStep].target;
|
||||
var pose = HeadPose.check(d.landmarks.positions, tgt, tgt === 'front' ? camMirrored : sideYawFlip);
|
||||
// 侧脸自校准:个别机型前置帧本身已镜像(语义 yaw 与实际转向持续相反)时自救——
|
||||
// 需连续 30 帧(~10s)强反向才翻转一次:阈值要高,否则用户转错方向 1~2s 就会被误判为镜像机,
|
||||
// 反而把错误的侧脸当合格抓走(左右图归属错乱)。翻转只动判定方向,不动预览画面
|
||||
if (tgt !== 'front' && pose.yaw != null && !mirrorCalibrated) {
|
||||
var wantSign = tgt === 'right' ? 1 : -1;
|
||||
if (pose.yaw * wantSign < -12) {
|
||||
if (++oppFrames >= 5) {
|
||||
camMirrored = !camMirrored;
|
||||
if (++oppFrames >= 30) {
|
||||
sideYawFlip = !sideYawFlip;
|
||||
mirrorCalibrated = true;
|
||||
oppFrames = 0;
|
||||
camVideo.classList.toggle('mirror', camMirrored);
|
||||
}
|
||||
} else oppFrames = 0;
|
||||
}
|
||||
@@ -528,13 +550,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── 拍照流程(三步:正脸 → 右脸出镜 → 左脸出镜) ─── */
|
||||
// target 即 HeadPose.check 的 target:2/3 右脸出镜=用户头向左转(target 'right',产物作 right_image);
|
||||
// 3/3 左脸出镜=头向右转(target 'left',产物作 left_image)。
|
||||
/* ─── 拍照流程(三步:正脸 → 左脸出镜 → 右脸出镜) ─── */
|
||||
// target 即 HeadPose.check 的 target:2/3 左脸出镜=用户头向右转(target 'left',产物作 left_image);
|
||||
// 3/3 右脸出镜=头向左转(target 'right',产物作 right_image)。
|
||||
var CAM_STEPS = [
|
||||
{ bg: 'assets/img/cam-front.jpg', step: '1/3<br>请单人正脸出镜', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front' },
|
||||
{ bg: 'assets/img/cam-left.jpg', step: '2/3<br>请单人右脸出镜', guides: ['右脸居中', '取下眼镜', '光线充足'], target: 'right' },
|
||||
{ bg: 'assets/img/cam-right.jpg', step: '3/3<br>请单人左脸出镜', guides: ['左脸居中', '取下眼镜', '光线充足'], target: 'left' }
|
||||
{ bg: 'assets/img/cam-left.jpg', step: '2/3<br>请单人左脸出镜', guides: ['左脸居中', '取下眼镜', '光线充足'], target: 'left' },
|
||||
{ bg: 'assets/img/cam-right.jpg', step: '3/3<br>请单人右脸出镜', guides: ['右脸居中', '取下眼镜', '光线充足'], target: 'right' }
|
||||
];
|
||||
var camStep = 0;
|
||||
|
||||
@@ -552,6 +574,7 @@
|
||||
dots[i].classList.toggle('on', i === camStep);
|
||||
}
|
||||
$('#cam-mask').setAttribute('data-step', camStep); // 蒙版按步骤切换
|
||||
camVideo.classList.toggle('mirror', effMirror()); // 侧脸步预览切回不镜像(正脸步恢复前置镜像)
|
||||
resetDetectState();
|
||||
voice.play(VOICE.step[camStep]); // 步骤引导语音
|
||||
}
|
||||
@@ -603,7 +626,7 @@
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 三张拍完:三视图送检(正脸=photos[0],左=photos[2],右=photos[1])
|
||||
// 三张拍完:三视图送检(正脸=photos[0],左=photos[1],右=photos[2])
|
||||
function submitPhotos(seq) {
|
||||
camStep = 0;
|
||||
openOverlay('p-analyzing');
|
||||
@@ -685,7 +708,7 @@
|
||||
fetch('/api/skin-analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ front_image: ph[0], left_image: ph[2], right_image: ph[1] }),
|
||||
body: JSON.stringify({ front_image: ph[0], left_image: ph[1], right_image: ph[2] }),
|
||||
signal: ctrl ? ctrl.signal : undefined // 老内核无 AbortController:仅靠令牌丢弃迟到响应
|
||||
}).then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
@@ -911,8 +934,9 @@
|
||||
for (i = 0; i < 3; i++) if (typeof ds[i] === 'number') { sum += ds[i]; n++; }
|
||||
if (n) total = Math.round(sum / n * 10);
|
||||
}
|
||||
$('#stat-score').textContent = total != null ? total + '分' : '--分';
|
||||
$('#stat-age').textContent = vm.skinAgeDisplay != null ? vm.skinAgeDisplay + '岁' : '--岁';
|
||||
// 设计稿:大数字 30px/400 + 单位 15px/500(分/岁为小字后缀)
|
||||
$('#stat-score').innerHTML = total != null ? '<b>' + total + '</b><i>分</i>' : '<b>--</b><i>分</i>';
|
||||
$('#stat-age').innerHTML = vm.skinAgeDisplay != null ? '<b>' + vm.skinAgeDisplay + '</b><i>岁</i>' : '<b>--</b><i>岁</i>';
|
||||
$('#stat-age-note').textContent = ''; // 肌龄小字按产品要求不展示(钳制留痕 vm.ageClampNote 仍随数据返回)
|
||||
|
||||
// 三维度:S 值(1 位小数),最低分标红(设计稿无档位徽章)
|
||||
@@ -954,17 +978,22 @@
|
||||
}
|
||||
|
||||
// 护理建议(V2.3 §8 五段:肤质定制/最差2分类/精华/生活方式);综合分析与生活方式通栏已按需求移除
|
||||
// 展示对齐 Figma:纯「序号、正文」,不显示段名【tag】、不加粗
|
||||
var careBox = $('#care-list');
|
||||
if (careBox) {
|
||||
careBox.innerHTML = (vm.careAdvice || []).map(function (s, i) {
|
||||
return '<p class="care-item"><b>' + (i + 1) + '、【' + esc(s.tag) + '】</b>' + esc(s.text) + '</p>';
|
||||
return '<p class="care-item">' + (i + 1) + '、' + esc(s.text) + '</p>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 精华推荐卡
|
||||
$('#essence-name').textContent = vm.essence.name;
|
||||
$('#essence-text').textContent = vm.essence.text;
|
||||
$('#essence-triggers').textContent = vm.essence.triggers.length ? '触发:' + vm.essence.triggers.join('、') : '';
|
||||
// 命中判据:引擎内部名 → 用户可读措辞(引擎值被真值回归逐字锁定,仅展示层映射)
|
||||
var TRIGGER_LABEL = { '均分档': '综合均分', '最差项约束': '最低分维度', '年龄组档': '年龄段' };
|
||||
$('#essence-triggers').textContent = vm.essence.triggers.length
|
||||
? '推荐依据:' + vm.essence.triggers.map(function (t) { return TRIGGER_LABEL[t] || t; }).join('、')
|
||||
: '';
|
||||
}
|
||||
|
||||
/* ─── 相机可用后加载检测模型(加载完成前快门即手动模式) ─── */
|
||||
|
||||
@@ -62,9 +62,10 @@
|
||||
var PITCH_COEFF = 180; // 归一化偏移 → 角度 的增益
|
||||
|
||||
// ---- check 阈值(角度制) ----
|
||||
// 正脸口径:Face++ 正脸判定 |yaw|<10°,前端留 2° 余量取 ≤12°
|
||||
var FRONT_YAW_MAX = 12;
|
||||
var FRONT_PITCH_MAX = 15;
|
||||
// 正脸口径:Face++ 正脸判定 |yaw|<10°;产品要求(2026-09-28)正脸引导放宽一点点,
|
||||
// 前端取 |yaw|≤15°、|pitch|≤18°(略超 Face++ 口径,正脸图仍可正常分析,仅 orientation 读数略偏)
|
||||
var FRONT_YAW_MAX = 15;
|
||||
var FRONT_PITCH_MAX = 18;
|
||||
var SIDE_YAW_MIN = 28;
|
||||
var SIDE_YAW_MAX = 75;
|
||||
var SIDE_PITCH_MAX = 20;
|
||||
@@ -156,7 +157,8 @@
|
||||
/**
|
||||
* check(points, target, mirrored) -> { ok, hint }
|
||||
* target: 'front' | 'left' | 'right'
|
||||
* 'front' : |yaw|<=12 且 |pitch|<=15
|
||||
* 'front' : |yaw|<=15 且 |pitch|<=18;不 ok 时一律 hint='请正对屏幕'
|
||||
* (产品要求 2026-09-28:正脸步不提示左右转头/抬头低头,只引导回正)
|
||||
* 'left' : 画面中看到左脸 = 被摄者头向右转(语义 yaw ∈ [-75°,-28°]),|pitch|<=20
|
||||
* 'right' : 画面中看到右脸 = 被摄者头向左转(语义 yaw ∈ [28°,75°]),|pitch|<=20
|
||||
* ok=true 时 hint='保持不动';输入非法(含 estimate 返回 null 的退化特征点)时 ok=false、hint='请正对屏幕'。
|
||||
@@ -179,11 +181,8 @@
|
||||
if (Math.abs(sYaw) <= FRONT_YAW_MAX && Math.abs(pitch) <= FRONT_PITCH_MAX) {
|
||||
return { ok: true, hint: HINT_KEEP, yaw: sYaw };
|
||||
}
|
||||
if (Math.abs(sYaw) > FRONT_YAW_MAX) {
|
||||
// 已向左转过头 → 提示向右回正,反之亦然
|
||||
return { ok: false, hint: sYaw > 0 ? HINT_TURN_RIGHT : HINT_TURN_LEFT, yaw: sYaw };
|
||||
}
|
||||
return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE, yaw: sYaw };
|
||||
// 正脸步只提示回正(产品要求:不区分左右转头/抬头低头,避免正脸阶段引导混乱)
|
||||
return { ok: false, hint: HINT_FRONT, yaw: sYaw };
|
||||
}
|
||||
|
||||
if (target === 'left' || target === 'right') {
|
||||
@@ -294,22 +293,22 @@ if (typeof module !== 'undefined' && typeof require !== 'undefined' && require.m
|
||||
ok(bc.ok === true, '反向侧脸 target=left ok');
|
||||
ok(HeadPose.check(back, 'right', false).ok === false, '反向侧脸 target=right 拒绝');
|
||||
|
||||
// 7) 侧脸做 front check → 拒绝并引导回正
|
||||
// 7) 侧脸做 front check → 拒绝并只引导回正(产品要求:正脸步不再提示左右转头)
|
||||
var sc = HeadPose.check(makeSide(), 'front', false);
|
||||
ok(sc.ok === false && sc.hint === '请向右转头', '侧脸 front 拒绝,hint=请向右转头');
|
||||
ok(sc.ok === false && sc.hint === '请正对屏幕', '侧脸 front 拒绝,hint=请正对屏幕');
|
||||
|
||||
// 8) roll:右眼(45 点)下移 10px → roll≈5.7°
|
||||
var tilted = makeFrontal();
|
||||
tilted[45] = { x: 250, y: 110 };
|
||||
ok(Math.abs(HeadPose.estimate(tilted, false).roll - 5.71) < 0.2, 'roll≈5.71°');
|
||||
|
||||
// 9) 低头:鼻底上移 → pitch<0,front 提示抬头
|
||||
// 9) 低头:鼻底上移 → pitch<0,front 拒绝并只提示回正(正脸步不再提示抬头/低头)
|
||||
var down = makeFrontal();
|
||||
down[33] = { x: 200, y: 100 };
|
||||
var de = HeadPose.estimate(down, false);
|
||||
var dc = HeadPose.check(down, 'front', false);
|
||||
ok(de.pitch < -30, '低头 pitch<0 (got ' + de.pitch.toFixed(2) + ')');
|
||||
ok(dc.ok === false && dc.hint === '请抬头', '低头 front 拒绝,hint=请抬头');
|
||||
ok(dc.ok === false && dc.hint === '请正对屏幕', '低头 front 拒绝,hint=请正对屏幕');
|
||||
|
||||
// 10) 非法输入:check 不抛异常,返回兜底引导
|
||||
var bad = HeadPose.check(null, 'front', false);
|
||||
@@ -328,19 +327,22 @@ if (typeof module !== 'undefined' && typeof require !== 'undefined' && require.m
|
||||
ok(HeadPose.estimate(tiny, false) === null, 'faceHeight 过小 → estimate 返回 null');
|
||||
ok(HeadPose.check(tiny, 'front', false).hint === '请正对屏幕', 'faceHeight 过小 check 兜底');
|
||||
|
||||
// 13) 正脸阈值收紧到 |yaw|≤12°(Face++ 口径 <10° + 2° 余量):yaw≈13.7°(旧阈值 15 内)应拒绝
|
||||
ok(HeadPose.config.FRONT_YAW_MAX === 12, 'config.FRONT_YAW_MAX = 12');
|
||||
// 13) 正脸阈值放宽到 |yaw|≤15°(产品要求 2026-09-28):yaw≈14.9° 应通过,yaw≈16.2° 应拒绝且只提示回正
|
||||
ok(HeadPose.config.FRONT_YAW_MAX === 15, 'config.FRONT_YAW_MAX = 15');
|
||||
var slight = makeFrontal();
|
||||
slight[30] = { x: 205.18, y: 140 }; // 偏移 5.18 → yaw≈13.7°(YAW_COEFF=3.8 标定)
|
||||
slight[33] = { x: 205.18, y: 161.5 };
|
||||
slight[30] = { x: 205.6, y: 140 }; // 偏移 5.6 → yaw≈14.9°(YAW_COEFF=3.8 标定)
|
||||
slight[33] = { x: 205.6, y: 161.5 };
|
||||
var se = HeadPose.estimate(slight, false);
|
||||
ok(se.yaw > 12 && se.yaw < 15, '轻微偏头 yaw∈(12,15) (got ' + se.yaw.toFixed(2) + ')');
|
||||
ok(se.yaw > 13.5 && se.yaw <= 15, '轻微偏头 yaw∈(13.5,15] (got ' + se.yaw.toFixed(2) + ')');
|
||||
var sc2 = HeadPose.check(slight, 'front', false);
|
||||
ok(sc2.ok === false, 'yaw≈13.7° front 拒绝(旧阈值 15 会放行)');
|
||||
ok(sc2.ok === true, 'yaw≈14.9°(≤15°)front 通过(旧阈值 12 会拒绝)');
|
||||
var slight2 = makeFrontal();
|
||||
slight2[30] = { x: 204.04, y: 140 }; // 偏移 4.04 → yaw≈10.7°
|
||||
slight2[33] = { x: 204.04, y: 161.5 };
|
||||
ok(HeadPose.check(slight2, 'front', false).ok === true, 'yaw≈10.7°(≤12°)front 通过');
|
||||
slight2[30] = { x: 206.2, y: 140 }; // 偏移 6.2 → yaw≈16.2°
|
||||
slight2[33] = { x: 206.2, y: 161.5 };
|
||||
var se2 = HeadPose.estimate(slight2, false);
|
||||
ok(se2.yaw > 15 && se2.yaw < 17.5, '偏头 yaw∈(15,17.5) (got ' + se2.yaw.toFixed(2) + ')');
|
||||
var sc3 = HeadPose.check(slight2, 'front', false);
|
||||
ok(sc3.ok === false && sc3.hint === '请正对屏幕', 'yaw≈16.2° front 拒绝,hint=请正对屏幕');
|
||||
|
||||
if (failures > 0) {
|
||||
console.error('\n' + failures + ' 项自测失败');
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
@@ -36,8 +36,8 @@ INSTRUCT = (
|
||||
# (文件名不含扩展名, 文案) — 与设计文档第 2 节逐字一致
|
||||
LINES: list[tuple[str, str]] = [
|
||||
("step1-front", "请单人正脸出镜,面部对准取景框"),
|
||||
("step2-rightface", "请保持单人出镜,慢慢向左转头,露出右脸"),
|
||||
("step3-leftface", "请慢慢向右转头,露出左脸"),
|
||||
("step2-leftface", "请慢慢向右转头,露出左脸"),
|
||||
("step3-rightface", "请保持单人出镜,慢慢向左转头,露出右脸"),
|
||||
("hint-noface", "未检测到人脸,请正对镜头"),
|
||||
("hint-near", "请靠近一点"),
|
||||
("hint-far", "请远一点"),
|
||||
|
||||