feat(h5): update skin scoring, age display and capture countdown
This commit is contained in:
+27
-7
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user