feat(h5): update skin scoring, age display and capture countdown

This commit is contained in:
xsl
2026-10-15 00:20:48 +08:00
parent d8c67762da
commit 60ae6c97ef
37 changed files with 5110 additions and 1632 deletions
+27 -7
View File
@@ -751,8 +751,8 @@
if (hard) stopCur();
}
// 等全部播报(含排队与挂起提示)自然收尾后执行 cb(倒计时接续「保持不动」,不拦腰切断);空闲则立即执行
function afterCurrent(cb) {
var tail = cur && cur.src && Date.now() >= cur.end - 60; // 只剩尾音视为已收尾
function afterCurrent(cb, waitForEnded) {
var tail = !waitForEnded && cur && cur.src && Date.now() >= cur.end - 60; // 倒计时必须等 ended,不能提前抓拍
if ((!cur || tail) && !queue.length && !pendingHint) { cb(); return null; }
var fn = function () { cb(); };
waiters.push(fn);
@@ -762,7 +762,7 @@
})();
document.addEventListener('click', voice.unlock); // 任意点击即完成解锁
/* ─── 姿态检测(face-api + HeadPose):动态捕捉——距离/居中/角度/光线全部合格并稳住片刻即自动抓拍,不倒计时、无手动快门 ─── */
/* ─── 姿态检测(face-api + HeadPose):全部合格并稳住片刻后播321,播完自动抓拍 ─── */
var MODEL_URL = 'vendor/faceapi/models';
var DETECT_INTERVAL = 150; // 检测帧间隔(ms)
var DETECT_INPUT_SIZE = 416; // tinyFaceDetector 输入尺寸:320 下正脸置信度仅 0.67 且框偏松,416 置信度 ~0.99 框更准(CPU 端略慢,移动端 GPU 无感)
@@ -1092,9 +1092,14 @@
clearTimeout(nudgeTimer);
hideNudge();
var seq = camSeq;
voice.dropHint();
voice.play(VOICE.countdown);
voice.afterCurrent(function () {
if (seq !== camSeq || !camLive || currentScreen !== 's-cam') return;
var fill = $('#cam-fill');
fill.classList.add('on'); // .on 无过渡,瞬间点亮
setTimeout(function () {
if (seq !== camSeq || !camLive || currentScreen !== 's-cam') { fill.classList.remove('on'); return; }
var shot = captureFromVideo();
setTimeout(function () {
fill.classList.remove('on'); // 移除 .on 恢复过渡,淡出
@@ -1112,6 +1117,7 @@
});
}, 300);
}, 300);
}, true);
}
// 三张拍完:按视角重排后送检(正脸=photos[0],左脸=photos[1],右脸=photos[2])
@@ -1254,7 +1260,7 @@
function setDim(key, dim, rank) {
var box = document.querySelector('.dim[data-dim="' + key + '"]');
var el = $('#dim-' + key), has = dim.S != null;
var parts = has ? dim.S.toFixed(1).split('.') : ['-', '-'];
var parts = has ? ReportUI.dimensionScore(dim.S).split('.') : ['-', '-'];
el.innerHTML = '<b>' + parts[0] + '</b>.' + parts[1];
el.classList.toggle('dim-bad', has && rank === 'worst');
box.classList.toggle('worst', has && rank === 'worst');
@@ -1276,7 +1282,10 @@
wrinkles: { img: '#ditem-img2', overlay: 'texture_enhanced_lines', dim: 0.55 }, // 皱纹:正脸压暗+皱纹增强线(图层几乎全透明,直挂在白卡上不可读,故合成)
smooth: { img: '#viz-smooth', overlays: ['texture_enhanced_pores', 'texture_enhanced_blackheads'] },
water: { img: '#viz-water', blendMap: 'water_area' },
tone: { img: '#viz-tone', blendMap: 'brown_area' }, // 肤色水润:正脸×水分图正片叠底(water_area,接口返回)
tone: { img: '#viz-tone', blendMap: 'brown_area', alpha: 0.3, marks: [
{ key: 'melanin_mark', color: '#9F641F' },
{ key: 'melasma', color: '#E68A2E' }, { key: 'freckle', color: '#8750C8' }
] }, // 仅使用接口真实坐标;黄褐斑/雀斑只有分类值时不画伪造框。
sens: { img: '#viz-sens', blendMap: 'red_area' }, // 敏感:正脸×红区图正片叠底(直挂白底过淡不可读)
acne: { img: '#viz-acne', photo: true, marks: [ // 痘痘:正脸+闭口粉刺/痤疮标记
{ key: 'closed_comedones', color: '#E8A13D' }, // 闭口粉刺
@@ -1390,15 +1399,19 @@
overlays.forEach(function (overlay) {
if (!overlay) return;
if (opts && opts.blend) ctx.globalCompositeOperation = opts.blend; // 正片叠底:白底图层(红区/色沉)叠到脸上
ctx.globalAlpha = opts && typeof opts.alpha === 'number' ? opts.alpha : 1;
var ox = overlay.naturalWidth / photo.naturalWidth, oy = overlay.naturalHeight / photo.naturalHeight;
ctx.drawImage(overlay, crop.left * ox, crop.top * oy, crop.width * ox, crop.height * oy, 0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
});
ctx.save();
ctx.translate(-crop.left * scale, -crop.top * scale);
var scaled = vizScaleMarks(marks, scale);
var lw = Math.max(3, Math.round(w / 200)); // 线宽随图走,卡片缩略后仍可见
var minBox = Math.round(w / 36); // 过小标记放大到最小框,保证缩略可见
var markAlpha = opts && typeof opts.alpha === 'number' ? opts.alpha : 1;
ctx.globalAlpha = markAlpha;
for (var i = 0; i < scaled.length; i++) {
var m = scaled[i];
ctx.strokeStyle = m.color;
@@ -1411,9 +1424,9 @@
ctx.moveTo(pts[0][0], pts[0][1]);
for (q = 1; q < pts.length; q++) ctx.lineTo(pts[q][0], pts[q][1]);
ctx.closePath();
ctx.globalAlpha = 0.22;
ctx.globalAlpha = markAlpha * 0.22;
ctx.fill();
ctx.globalAlpha = 1;
ctx.globalAlpha = markAlpha;
ctx.stroke();
}
for (p = 0; p < m.rects.length; p++) {
@@ -1427,6 +1440,7 @@
try {
imgEl.src = cv.toDataURL('image/jpeg', 0.9); imgEl.hidden = false;
imgEl.dataset.crop = crop.focused ? 'face' : 'full';
imgEl.dataset.overlayAlpha = String(opts && typeof opts.alpha === 'number' ? opts.alpha : 1);
} catch (e) { /* 合成失败保留占位 */ }
};
var urls = Array.isArray(overlayURL) ? overlayURL : overlayURL ? [overlayURL] : [];
@@ -1466,6 +1480,12 @@
}
if (cfg.blendMap) { // 图层×正脸正片叠底(色沉/红区):无照片时直挂图层;图层缺失 → 保留占位
var bu = vizMapURL(maps[cfg.blendMap]);
if (key === 'tone') {
var toneDefs = bu ? cfg.marks.filter(function(def) { return def.key !== 'melanin_mark'; }) : cfg.marks;
var toneMarks = vizCollectMarks(toneDefs, result);
if (frontPhoto) vizCompose(frontPhoto, bu, toneMarks, imgEl, { blend: 'multiply', alpha: cfg.alpha, faceRectangle: rectangle });
continue;
}
if (!bu) continue;
if (!frontPhoto) { imgEl.src = bu; imgEl.hidden = false; continue; }
vizCompose(frontPhoto, bu, [], imgEl, { blend: 'multiply', faceRectangle: rectangle });