feat: 真机反馈 13 项修改(下拉/拍照页/语音时机/预热/报告样式/翻转镜头)
1. 城市改自定义下拉(原生 select 各端样式不一) 2. 姿态提示浮层下移,不再压住脸部轮廓 3. 倒计时 321 前不再抢播「保持不动」(被切断听着像吞字) 4. 相机预热:分辨率稳定两拍后才出实时画面+放开检测,开头 1-2s 大脸闪变消除 5. 隐私协议页去掉头部文字条,返回键改悬浮 6. 肌龄小字常显「与实际年龄相差X岁」,钳制时追加「触发(原X→Y)」(vm 增 realAge 字段) 7. 删除设计稿没有的「6个维度观察/状态动态流动」两段文字 8. 维度卡关注行改设计稿两行式:灰小字+深色口号(稳定力口号用设计稿原文,另两条待确认) 9. Tab 黄色指示条宽度跟随文字(原固定 78px) 10. 抓拍只取取景框可见区域(object-fit cover 中心裁切),照片=所见画幅;拍照页顶栏黑条删除;无进展提示上移不再压快门 11. 相册选择器「拍照」项为系统行为,代码层已是图库意图(无 capture),详见回复 12. 切换镜头实现:前后置切换+镜像/姿态参数跟随,替换 Toast 占位 13. 报告分类卡去掉得分与档位徽章(设计稿只有话术),图片改 100px 见方圆角 10 e2e 全量绿(E0-E7)
This commit is contained in:
@@ -594,3 +594,61 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
|||||||
.his-score b { font-size: 22px; font-weight: 500; color: var(--ink); }
|
.his-score b { font-size: 22px; font-weight: 500; color: var(--ink); }
|
||||||
.his-score span { font-size: 11px; color: var(--grey); margin-left: 2px; }
|
.his-score span { font-size: 11px; color: var(--grey); margin-left: 2px; }
|
||||||
.his-del { flex: none; font-size: 11px; color: var(--grey-2); padding: 6px 4px; }
|
.his-del { flex: none; font-size: 11px; color: var(--grey-2); padding: 6px 4px; }
|
||||||
|
|
||||||
|
/* ═══════════ 第6轮:真机反馈 13 项 ═══════════ */
|
||||||
|
/* #2 姿态提示浮层:移到取景框下方(原 29% 与脸部轮廓/下巴重叠) */
|
||||||
|
.cam-hint { bottom: 21%; }
|
||||||
|
/* #10 无进展提示与快门重叠:上移 */
|
||||||
|
.cam-nudge { bottom: 16%; }
|
||||||
|
/* #4/#10 拍照页顶栏取消,返回键改悬浮白圆(对齐设计稿) */
|
||||||
|
.cam-back-float {
|
||||||
|
position: absolute; top: 12px; left: 12px; z-index: 3;
|
||||||
|
background: rgba(255,255,255,.9); border-color: transparent;
|
||||||
|
}
|
||||||
|
/* #12 预览镜像改为类切换(前置镜像/后置不镜像) */
|
||||||
|
#cam-video { transform: none; }
|
||||||
|
#cam-video.mirror { transform: scaleX(-1); }
|
||||||
|
|
||||||
|
/* #1 城市自定义下拉(替代原生 select) */
|
||||||
|
.rs { position: relative; }
|
||||||
|
.rs-current {
|
||||||
|
width: 100%; display: flex; align-items: center; justify-content: space-between;
|
||||||
|
background: #fff; border: 1px solid var(--line); border-radius: 8px;
|
||||||
|
padding: 12px 14px; font-size: 13px; color: var(--ink); text-align: left;
|
||||||
|
}
|
||||||
|
.rs-current .chev { transition: transform .2s; }
|
||||||
|
.rs.open .rs-current .chev { transform: rotate(225deg); }
|
||||||
|
.rs-value.rs-placeholder { color: #B3B3B3; }
|
||||||
|
.rs-list {
|
||||||
|
position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 30;
|
||||||
|
max-height: 264px; overflow-y: auto; -webkit-overflow-scrolling: touch;
|
||||||
|
background: #fff; border: 1px solid var(--line); border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
.rs-list li { padding: 12px 14px; font-size: 13px; color: var(--ink); }
|
||||||
|
.rs-list li + li { border-top: 1px solid #F3F3F3; }
|
||||||
|
.rs-list li:active { background: #F7F7F7; }
|
||||||
|
|
||||||
|
/* #5 隐私页去头部条,返回键悬浮 */
|
||||||
|
.policy-float-back {
|
||||||
|
position: absolute; top: 12px; left: 12px; z-index: 2;
|
||||||
|
background: #fff; box-shadow: 0 1px 6px rgba(0,0,0,.12);
|
||||||
|
}
|
||||||
|
.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; }
|
||||||
|
|||||||
+12
-25
@@ -58,12 +58,14 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label">您的所在地区</label>
|
<label class="field-label">您的所在地区</label>
|
||||||
<div class="field-select-wrap">
|
<div class="rs" id="region-rs">
|
||||||
<select id="f-region" class="field-input field-select">
|
<button type="button" class="rs-current" id="rs-current">
|
||||||
<option value="" selected disabled>请选择</option>
|
<span id="rs-value" class="rs-value rs-placeholder">请选择</span>
|
||||||
</select>
|
<i class="chev down"></i>
|
||||||
<i class="chev down"></i>
|
</button>
|
||||||
|
<ul class="rs-list" id="rs-list" hidden></ul>
|
||||||
</div>
|
</div>
|
||||||
|
<input type="hidden" id="f-region">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -88,10 +90,7 @@
|
|||||||
<!-- ═══════════ 05-07 拍照页 ═══════════ -->
|
<!-- ═══════════ 05-07 拍照页 ═══════════ -->
|
||||||
<section class="screen" id="s-cam">
|
<section class="screen" id="s-cam">
|
||||||
<div class="cam-bg" id="cam-bg"></div>
|
<div class="cam-bg" id="cam-bg"></div>
|
||||||
<header class="nav nav-dark">
|
<button class="nav-back nav-dark cam-back-float" data-action="cam-back" aria-label="返回"><i class="chev left"></i></button>
|
||||||
<button class="nav-back" data-action="cam-back" aria-label="返回"><i class="chev left"></i></button>
|
|
||||||
<span class="nav-title">1'me在线肌肤测试</span>
|
|
||||||
</header>
|
|
||||||
<div class="cam-dots" id="cam-dots"><i></i><i></i><i></i></div>
|
<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>
|
<p class="cam-step" id="cam-step">1/3<br>请单人正脸出镜</p>
|
||||||
<!-- 取景蒙版:SVG mask 镂空压暗 + 轮廓线框;data-step 切换三种轮廓
|
<!-- 取景蒙版:SVG mask 镂空压暗 + 轮廓线框;data-step 切换三种轮廓
|
||||||
@@ -158,9 +157,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="dims-intro">我们从6个维度观察了你的肌肤,当下优先关注:紧致、光泽、纹路、耐受、细腻、水润</p>
|
|
||||||
<p class="dims-disclaimer">*状态是动态流动的,和作息、压力、季节都有关系,仅代表此刻的状态参考。</p>
|
|
||||||
|
|
||||||
<div class="card card-dims">
|
<div class="card card-dims">
|
||||||
<div class="dims-row">
|
<div class="dims-row">
|
||||||
<div class="dim"><span class="dim-name">紧塑力</span><span class="dim-score" id="dim-firm">-.-</span><span class="dim-lv" id="dim-firm-lv"></span></div>
|
<div class="dim"><span class="dim-name">紧塑力</span><span class="dim-score" id="dim-firm">-.-</span><span class="dim-lv" id="dim-firm-lv"></span></div>
|
||||||
@@ -172,9 +168,9 @@
|
|||||||
|
|
||||||
<div class="card card-detail">
|
<div class="card card-detail">
|
||||||
<div class="detail-tabs" id="detail-tabs">
|
<div class="detail-tabs" id="detail-tabs">
|
||||||
<button class="dtab active" data-tab="0">紧塑力</button>
|
<button class="dtab active" data-tab="0"><span class="dtab-tx">紧塑力</span></button>
|
||||||
<button class="dtab" data-tab="1">活力感</button>
|
<button class="dtab" data-tab="1"><span class="dtab-tx">活力感</span></button>
|
||||||
<button class="dtab" data-tab="2">稳定力</button>
|
<button class="dtab" data-tab="2"><span class="dtab-tx">稳定力</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-panels">
|
<div class="detail-panels">
|
||||||
<div class="dpanel active" data-panel="0">
|
<div class="dpanel active" data-panel="0">
|
||||||
@@ -182,7 +178,6 @@
|
|||||||
<h3 class="ditem-title">轮廓</h3>
|
<h3 class="ditem-title">轮廓</h3>
|
||||||
<img id="ditem-img1" src="assets/img/det-1.jpg" loading="lazy" class="ditem-img" alt="轮廓检测图">
|
<img id="ditem-img1" src="assets/img/det-1.jpg" loading="lazy" class="ditem-img" alt="轮廓检测图">
|
||||||
<div class="ditem-text">
|
<div class="ditem-text">
|
||||||
<p class="cat-head"><span class="cat-s" id="s-contour">-.-</span><i class="cat-badge" id="lv-contour"></i></p>
|
|
||||||
<p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p>
|
<p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +185,6 @@
|
|||||||
<h3 class="ditem-title">皱纹</h3>
|
<h3 class="ditem-title">皱纹</h3>
|
||||||
<img id="ditem-img2" src="assets/img/det-2.jpg" loading="lazy" class="ditem-img" alt="皱纹检测图">
|
<img id="ditem-img2" src="assets/img/det-2.jpg" loading="lazy" class="ditem-img" alt="皱纹检测图">
|
||||||
<div class="ditem-text">
|
<div class="ditem-text">
|
||||||
<p class="cat-head"><span class="cat-s" id="s-wrinkles">-.-</span><i class="cat-badge" id="lv-wrinkles"></i></p>
|
|
||||||
<p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p>
|
<p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +194,6 @@
|
|||||||
<h3 class="ditem-title">光滑</h3>
|
<h3 class="ditem-title">光滑</h3>
|
||||||
<img id="viz-smooth" loading="lazy" class="ditem-img" alt="光滑检测标记图" hidden>
|
<img id="viz-smooth" loading="lazy" class="ditem-img" alt="光滑检测标记图" hidden>
|
||||||
<div class="ditem-text">
|
<div class="ditem-text">
|
||||||
<p class="cat-head"><span class="cat-s" id="s-smooth">-.-</span><i class="cat-badge" id="lv-smooth"></i></p>
|
|
||||||
<p class="ditem-para" id="tx-smooth">检测完成后显示分析结果。</p>
|
<p class="ditem-para" id="tx-smooth">检测完成后显示分析结果。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +201,6 @@
|
|||||||
<h3 class="ditem-title">肤色水润</h3>
|
<h3 class="ditem-title">肤色水润</h3>
|
||||||
<img id="viz-tone" loading="lazy" class="ditem-img" alt="肤色色沉图" hidden>
|
<img id="viz-tone" loading="lazy" class="ditem-img" alt="肤色色沉图" hidden>
|
||||||
<div class="ditem-text">
|
<div class="ditem-text">
|
||||||
<p class="cat-head"><span class="cat-s" id="s-tone">-.-</span><i class="cat-badge" id="lv-tone"></i></p>
|
|
||||||
<p class="ditem-para" id="tx-tone">检测完成后显示分析结果。</p>
|
<p class="ditem-para" id="tx-tone">检测完成后显示分析结果。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +210,6 @@
|
|||||||
<h3 class="ditem-title">敏感</h3>
|
<h3 class="ditem-title">敏感</h3>
|
||||||
<img id="viz-sens" loading="lazy" class="ditem-img" alt="敏感红区图" hidden>
|
<img id="viz-sens" loading="lazy" class="ditem-img" alt="敏感红区图" hidden>
|
||||||
<div class="ditem-text">
|
<div class="ditem-text">
|
||||||
<p class="cat-head"><span class="cat-s" id="s-sens">-.-</span><i class="cat-badge" id="lv-sens"></i></p>
|
|
||||||
<p class="ditem-para" id="tx-sens">检测完成后显示分析结果。</p>
|
<p class="ditem-para" id="tx-sens">检测完成后显示分析结果。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +217,6 @@
|
|||||||
<h3 class="ditem-title">痘痘</h3>
|
<h3 class="ditem-title">痘痘</h3>
|
||||||
<img id="viz-acne" loading="lazy" class="ditem-img" alt="痘痘检测标记图" hidden>
|
<img id="viz-acne" loading="lazy" class="ditem-img" alt="痘痘检测标记图" hidden>
|
||||||
<div class="ditem-text">
|
<div class="ditem-text">
|
||||||
<p class="cat-head"><span class="cat-s" id="s-acne">-.-</span><i class="cat-badge" id="lv-acne"></i></p>
|
|
||||||
<p class="ditem-para" id="tx-acne">检测完成后显示分析结果。</p>
|
<p class="ditem-para" id="tx-acne">检测完成后显示分析结果。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,10 +294,7 @@
|
|||||||
<!-- ═══════════ 弹层:隐私政策与服务协议(全文) ═══════════ -->
|
<!-- ═══════════ 弹层:隐私政策与服务协议(全文) ═══════════ -->
|
||||||
<div class="overlay overlay-policy" id="p-policy">
|
<div class="overlay overlay-policy" id="p-policy">
|
||||||
<div class="policy-page">
|
<div class="policy-page">
|
||||||
<header class="policy-header">
|
<button class="nav-back policy-float-back" data-action="close-policy" aria-label="返回"><i class="chev left"></i></button>
|
||||||
<button class="nav-back" data-action="close-policy" aria-label="返回"><i class="chev left"></i></button>
|
|
||||||
<span class="policy-header-title">隐私政策与服务协议</span>
|
|
||||||
</header>
|
|
||||||
<div class="policy-body">
|
<div class="policy-body">
|
||||||
<h1 class="policy-title">Face⁺⁺个人信息与隐私保护政策</h1>
|
<h1 class="policy-title">Face⁺⁺个人信息与隐私保护政策</h1>
|
||||||
|
|
||||||
|
|||||||
+81
-37
@@ -142,14 +142,27 @@
|
|||||||
if (drag.suppress) { e.preventDefault(); e.stopPropagation(); drag.suppress = false; }
|
if (drag.suppress) { e.preventDefault(); e.stopPropagation(); drag.suppress = false; }
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
/* ─── 地区选项 ─── */
|
/* ─── 地区选项(自定义下拉:原生 select 各端样式不一,改自绘弹层) ─── */
|
||||||
var REGIONS = ['北京', '上海', '广州', '深圳', '杭州', '成都', '重庆', '武汉', '西安', '南京', '天津', '苏州', '长沙', '郑州', '青岛', '其他'];
|
var REGIONS = ['北京', '上海', '广州', '深圳', '杭州', '成都', '重庆', '武汉', '西安', '南京', '天津', '苏州', '长沙', '郑州', '青岛', '其他'];
|
||||||
var regionSel = $('#f-region');
|
var regionSel = $('#f-region'); // hidden input,存选中值
|
||||||
REGIONS.forEach(function (r) {
|
var rsBox = $('#region-rs'), rsList = $('#rs-list'), rsValue = $('#rs-value');
|
||||||
var o = document.createElement('option');
|
rsList.innerHTML = REGIONS.map(function (r) { return '<li data-v="' + r + '">' + r + '</li>'; }).join('');
|
||||||
o.textContent = r;
|
function rsClose() { rsList.hidden = true; rsBox.classList.remove('open'); }
|
||||||
regionSel.appendChild(o);
|
$('#rs-current').addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var willOpen = rsList.hidden;
|
||||||
|
rsList.hidden = !willOpen;
|
||||||
|
rsBox.classList.toggle('open', willOpen);
|
||||||
});
|
});
|
||||||
|
rsList.addEventListener('click', function (e) {
|
||||||
|
var li = e.target.closest('li[data-v]');
|
||||||
|
if (!li) return;
|
||||||
|
regionSel.value = li.dataset.v;
|
||||||
|
rsValue.textContent = li.dataset.v;
|
||||||
|
rsValue.classList.remove('rs-placeholder');
|
||||||
|
rsClose();
|
||||||
|
});
|
||||||
|
document.addEventListener('click', rsClose); // 点空白收起
|
||||||
|
|
||||||
/* ─── 实时相机(getUserMedia)+ 系统相机降级 ─── */
|
/* ─── 实时相机(getUserMedia)+ 系统相机降级 ─── */
|
||||||
var IS_MOCK = /[?&]mock=1/.test(location.search); // mock:不加载 face-api、不跑检测,analyze 直返内置样本
|
var IS_MOCK = /[?&]mock=1/.test(location.search); // mock:不加载 face-api、不跑检测,analyze 直返内置样本
|
||||||
@@ -158,9 +171,11 @@
|
|||||||
var photos = []; // 三连拍(dataURL),与 CAM_STEPS 同序:[0]正脸 [1]右脸(头左转) [2]左脸(头右转)
|
var photos = []; // 三连拍(dataURL),与 CAM_STEPS 同序:[0]正脸 [1]右脸(头左转) [2]左脸(头右转)
|
||||||
|
|
||||||
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
|
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
|
||||||
|
var camFacing = 'user'; // 当前镜头:'user' 前置 | 'environment' 后置(翻转按钮切换)
|
||||||
|
var camMirrored = true; // 预览是否镜像(前置镜像,后置不镜像)
|
||||||
function getCamStream() {
|
function getCamStream() {
|
||||||
return navigator.mediaDevices.getUserMedia({
|
return navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: { ideal: 'user' }, width: { ideal: 1280 }, height: { ideal: 1280 } },
|
video: { facingMode: { ideal: camFacing }, width: { ideal: 1280 }, height: { ideal: 1280 } },
|
||||||
audio: false
|
audio: false
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (err && err.name === 'OverconstrainedError') {
|
if (err && err.name === 'OverconstrainedError') {
|
||||||
@@ -169,6 +184,18 @@
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 相机预热:部分设备开头 1~2s 以低分辨率出画(画面显脸巨大),等分辨率稳定两拍再放开取景与检测
|
||||||
|
function waitVideoStable(done) {
|
||||||
|
var lastW = 0, stableN = 0, tries = 0;
|
||||||
|
var t = setInterval(function () {
|
||||||
|
var w = camVideo.videoWidth;
|
||||||
|
tries++;
|
||||||
|
if (w && w === lastW) {
|
||||||
|
if (++stableN >= 2) { clearInterval(t); done(); }
|
||||||
|
} else { stableN = 0; lastW = w; }
|
||||||
|
if (tries > 40) { clearInterval(t); done(); } // 12s 兜底,异常也不卡死
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
function startCamera() {
|
function startCamera() {
|
||||||
if (camStream) { stopCamera(); }
|
if (camStream) { stopCamera(); }
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { enableFallback(); return; }
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { enableFallback(); return; }
|
||||||
@@ -178,14 +205,18 @@
|
|||||||
if (token !== camSeqCam) { stream.getTracks().forEach(function (t) { t.stop(); }); return; } // 期间已退出拍照页:立即释放
|
if (token !== camSeqCam) { stream.getTracks().forEach(function (t) { t.stop(); }); return; } // 期间已退出拍照页:立即释放
|
||||||
camStream = stream;
|
camStream = stream;
|
||||||
camVideo.srcObject = stream;
|
camVideo.srcObject = stream;
|
||||||
camLive = true;
|
camVideo.classList.toggle('mirror', camMirrored);
|
||||||
// iOS 来电/切后台等系统打断会结束 track:挂 ended 监听走自动重连
|
// iOS 来电/切后台等系统打断会结束 track:挂 ended 监听走自动重连
|
||||||
stream.getTracks().forEach(function (t) {
|
stream.getTracks().forEach(function (t) {
|
||||||
t.onended = function () { onCamTrackEnded(token); };
|
t.onended = function () { onCamTrackEnded(token); };
|
||||||
});
|
});
|
||||||
$('#s-cam').classList.add('cam-live');
|
waitVideoStable(function () { // 预热完成前不出实时画面(静态示意底图还在),避免开头"大脸"闪变
|
||||||
beginDetect(); // 确认相机可用后才加载检测模型(无相机权限设备不白加载)
|
if (token !== camSeqCam) return;
|
||||||
maybeStartDetect(); // 模型已就绪时即刻开跑检测循环
|
camLive = true;
|
||||||
|
$('#s-cam').classList.add('cam-live');
|
||||||
|
beginDetect(); // 确认相机可用后才加载检测模型(无相机权限设备不白加载)
|
||||||
|
maybeStartDetect(); // 模型已就绪时即刻开跑检测循环
|
||||||
|
});
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
if (token !== camSeqCam) return;
|
if (token !== camSeqCam) return;
|
||||||
enableFallback();
|
enableFallback();
|
||||||
@@ -218,13 +249,18 @@
|
|||||||
function captureFromVideo() {
|
function captureFromVideo() {
|
||||||
var vw = camVideo.videoWidth, vh = camVideo.videoHeight;
|
var vw = camVideo.videoWidth, vh = camVideo.videoHeight;
|
||||||
if (!vw || !vh) return null;
|
if (!vw || !vh) return null;
|
||||||
var scale = Math.min(1, 1280 / Math.max(vw, vh));
|
// 只取取景框可见区域(object-fit:cover 的中心裁切):照片 = 用户当时看到的画面
|
||||||
var cw = Math.round(vw * scale), ch = Math.round(vh * scale);
|
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 canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = cw; canvas.height = ch;
|
canvas.width = cw; canvas.height = ch;
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
ctx.translate(cw, 0); ctx.scale(-1, 1); // 镜像
|
if (camMirrored) { ctx.translate(cw, 0); ctx.scale(-1, 1); } // 前置镜像,与预览一致
|
||||||
ctx.drawImage(camVideo, 0, 0, cw, ch);
|
ctx.drawImage(camVideo, sx, sy, visW, visH, 0, 0, cw, ch);
|
||||||
return canvas.toDataURL('image/jpeg', 0.88);
|
return canvas.toDataURL('image/jpeg', 0.88);
|
||||||
}
|
}
|
||||||
// 文件 → 统一压成最长边 1280 的 JPEG → 推进流程(降级系统相机与相册选图共用)
|
// 文件 → 统一压成最长边 1280 的 JPEG → 推进流程(降级系统相机与相册选图共用)
|
||||||
@@ -343,12 +379,12 @@
|
|||||||
m.classList.toggle('no-face', cls === 'no-face');
|
m.classList.toggle('no-face', cls === 'no-face');
|
||||||
m.classList.toggle('ok', cls === 'ok');
|
m.classList.toggle('ok', cls === 'ok');
|
||||||
}
|
}
|
||||||
function setHint(text, ok) {
|
function setHint(text, ok, silent) {
|
||||||
var h = $('#cam-hint');
|
var h = $('#cam-hint');
|
||||||
if (!text) { h.classList.remove('show', 'ok'); lastHint = ''; return; }
|
if (!text) { h.classList.remove('show', 'ok'); lastHint = ''; return; }
|
||||||
if (text !== lastHint) {
|
if (text !== lastHint) {
|
||||||
h.textContent = text; // 同文案不重复触发布局
|
h.textContent = text; // 同文案不重复触发布局
|
||||||
voice.play(VOICE.hint[text]); // 语音同步(模块内 4s 去重)
|
if (!silent) voice.play(VOICE.hint[text]); // 语音同步(模块内 4s 去重)
|
||||||
}
|
}
|
||||||
h.classList.add('show');
|
h.classList.add('show');
|
||||||
h.classList.toggle('ok', !!ok);
|
h.classList.toggle('ok', !!ok);
|
||||||
@@ -388,7 +424,7 @@
|
|||||||
c.textContent = cdN;
|
c.textContent = cdN;
|
||||||
c.classList.add('show');
|
c.classList.add('show');
|
||||||
setMask('ok'); // 轮廓保持绿色
|
setMask('ok'); // 轮廓保持绿色
|
||||||
setHint('保持不动', true);
|
setHint('保持不动', true, /* silent= */true); // 倒计时前不再抢播「保持不动」(会被 321 立刻切断,听着像吞字)
|
||||||
voice.play(VOICE.countdown); // 「三、二、一」与数字同步起播
|
voice.play(VOICE.countdown); // 「三、二、一」与数字同步起播
|
||||||
cdTimer = setTimeout(cdTick, 1000);
|
cdTimer = setTimeout(cdTick, 1000);
|
||||||
}
|
}
|
||||||
@@ -456,8 +492,8 @@
|
|||||||
if (ratio < FACE_NEAR) hint = '请靠近一点';
|
if (ratio < FACE_NEAR) hint = '请靠近一点';
|
||||||
else if (ratio > FACE_FAR) hint = '请远一点';
|
else if (ratio > FACE_FAR) hint = '请远一点';
|
||||||
else {
|
else {
|
||||||
// 前置预览为镜像画面 → mirrored=true;距离不合格时角度不判 ok
|
// 镜像标志跟随镜头(前置 true/后置 false);距离不合格时角度不判 ok
|
||||||
var pose = HeadPose.check(d.landmarks.positions, CAM_STEPS[camStep].target, true);
|
var pose = HeadPose.check(d.landmarks.positions, CAM_STEPS[camStep].target, camMirrored);
|
||||||
hint = pose.hint;
|
hint = pose.hint;
|
||||||
ok = pose.ok;
|
ok = pose.ok;
|
||||||
}
|
}
|
||||||
@@ -680,19 +716,8 @@
|
|||||||
|
|
||||||
function setCat(key, cat) {
|
function setCat(key, cat) {
|
||||||
var dom = CAT_DOM[key];
|
var dom = CAT_DOM[key];
|
||||||
var s = $(dom.s), lv = $(dom.lv);
|
// 设计稿分类卡只有话术正文:不展示 S 与档位徽章;缺数据分类以说明代正文
|
||||||
if (cat.P == null) { // 缺数据分类:显示说明,不显示分数
|
$(dom.tx).textContent = cat.P == null ? '本次未检出有效数据' : cat.text;
|
||||||
s.textContent = '本次未检出有效数据';
|
|
||||||
s.className = 'cat-s cat-na';
|
|
||||||
lv.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
s.textContent = cat.S.toFixed(1);
|
|
||||||
s.className = 'cat-s';
|
|
||||||
lv.style.display = '';
|
|
||||||
lv.textContent = cat.level;
|
|
||||||
lv.className = 'cat-badge ' + (LV_CLASS[cat.level] || '');
|
|
||||||
}
|
|
||||||
$(dom.tx).textContent = cat.text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── 报告页检测可视化(face_maps 图层 + marks 坐标叠加,数据驱动) ─── */
|
/* ─── 报告页检测可视化(face_maps 图层 + marks 坐标叠加,数据驱动) ─── */
|
||||||
@@ -866,15 +891,28 @@
|
|||||||
}
|
}
|
||||||
$('#stat-score').textContent = total != null ? total + '分' : '--分';
|
$('#stat-score').textContent = total != null ? total + '分' : '--分';
|
||||||
$('#stat-age').textContent = vm.skinAgeDisplay != null ? vm.skinAgeDisplay + '岁' : '--岁';
|
$('#stat-age').textContent = vm.skinAgeDisplay != null ? vm.skinAgeDisplay + '岁' : '--岁';
|
||||||
$('#stat-age-note').textContent = vm.ageClampNote || ''; // 肤龄钳制留痕(小字灰)
|
// 肌龄小字:常显「与实际年龄相差X岁」,触发钳制时追加留痕(契约格式「触发(原X→Y)」不变)
|
||||||
|
var ageNote = '';
|
||||||
|
if (vm.skinAgeDisplay != null && vm.realAge != null) {
|
||||||
|
var ageDiff = Math.abs(vm.skinAgeDisplay - vm.realAge);
|
||||||
|
ageNote = ageDiff === 0 ? '与实际年龄相符' : '与实际年龄相差' + ageDiff + '岁';
|
||||||
|
}
|
||||||
|
if (vm.ageClampNote) ageNote += (ageNote ? ' · ' : '') + vm.ageClampNote;
|
||||||
|
$('#stat-age-note').textContent = ageNote;
|
||||||
|
|
||||||
// 三维度:S 值(1 位小数) + 档位(良好绿/改善琥珀/预警红)
|
// 三维度:S 值(1 位小数) + 档位(良好绿/改善琥珀/预警红)
|
||||||
setDim('#dim-firm', '#dim-firm-lv', vm.dims.firm);
|
setDim('#dim-firm', '#dim-firm-lv', vm.dims.firm);
|
||||||
setDim('#dim-vita', '#dim-vita-lv', vm.dims.vita);
|
setDim('#dim-vita', '#dim-vita-lv', vm.dims.vita);
|
||||||
setDim('#dim-stab', '#dim-stab-lv', vm.dims.stab);
|
setDim('#dim-stab', '#dim-stab-lv', vm.dims.stab);
|
||||||
|
// 关注行(设计稿两行:灰小字「近期优先关注:X」+ 深色维度口号)
|
||||||
|
var DIM_SLOGAN = {
|
||||||
|
'紧塑力': '紧塑提拉力,轮廓上扬不松垮',
|
||||||
|
'活力感': '活力透亮感,水润澎弹有光泽',
|
||||||
|
'稳定力': '强韧稳态力,锁水保湿抵御刺激' // 设计稿原文
|
||||||
|
};
|
||||||
$('#dims-note').innerHTML = vm.focus.name
|
$('#dims-note').innerHTML = vm.focus.name
|
||||||
? '近期优先关注:' + esc(vm.focus.name) + '<br>' + esc(vm.focus.tip)
|
? '<span class="dims-note-l1">近期优先关注:' + esc(vm.focus.name) + '</span><br><span class="dims-note-l2">' + esc(DIM_SLOGAN[vm.focus.name] || '') + '</span>'
|
||||||
: '近期优先关注:<br>---';
|
: '<span class="dims-note-l1">近期优先关注:---</span>';
|
||||||
|
|
||||||
// 三个 Tab 面板:各所辖两个分类(S + 档位徽章 + 话术正文)
|
// 三个 Tab 面板:各所辖两个分类(S + 档位徽章 + 话术正文)
|
||||||
var keys = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
|
var keys = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
|
||||||
@@ -990,7 +1028,13 @@
|
|||||||
shutterLock = true; // 与降级快门同策略:前置上锁,change/cancel 解锁
|
shutterLock = true; // 与降级快门同策略:前置上锁,change/cancel 解锁
|
||||||
albumFile.click();
|
albumFile.click();
|
||||||
},
|
},
|
||||||
'flip': function () { toast('切换镜头功能开发中'); },
|
'flip': function () { // 切换前后镜头
|
||||||
|
if (camFallback) { toast('系统相机模式下请在系统相机内切换'); return; }
|
||||||
|
if (!camStream) { toast('相机启动中,请稍候'); return; }
|
||||||
|
camFacing = camFacing === 'user' ? 'environment' : 'user';
|
||||||
|
camMirrored = camFacing === 'user';
|
||||||
|
startCamera(); // 复用启停与预热(camSeqCam 防竞态)
|
||||||
|
},
|
||||||
'back-profile': function () { showScreen('s-profile'); },
|
'back-profile': function () { showScreen('s-profile'); },
|
||||||
'history': function () { openHistory(); },
|
'history': function () { openHistory(); },
|
||||||
'close-history': function () { closeOverlay('p-history'); },
|
'close-history': function () { closeOverlay('p-history'); },
|
||||||
|
|||||||
@@ -722,6 +722,7 @@
|
|||||||
skinAgeRaw: skinAge,
|
skinAgeRaw: skinAge,
|
||||||
skinAgeDisplay: skinAgeDisplay,
|
skinAgeDisplay: skinAgeDisplay,
|
||||||
ageClampNote: ageClampNote,
|
ageClampNote: ageClampNote,
|
||||||
|
realAge: realAge,
|
||||||
dims: dims,
|
dims: dims,
|
||||||
cats: cats,
|
cats: cats,
|
||||||
group: group,
|
group: group,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 926 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
Reference in New Issue
Block a user