feat(h5): apply second customer UI feedback and homepage refinements

This commit is contained in:
xsl
2026-10-12 14:35:39 +08:00
parent abb1c4c1de
commit fe9623a1d7
16 changed files with 1963 additions and 1082 deletions
+55 -7
View File
@@ -4,10 +4,43 @@
var $ = function (sel, root) { return (root || document).querySelector(sel); };
/* camera-outline-pure-begin */
// Round the exported polygon's corners without changing its position/size.
// Quadratic control points stay within the original corner's convex hull.
function smoothCameraOutline(outline) {
if (typeof outline !== 'string' || !/^M[\d.,\sL+-]+Z$/i.test(outline)) return outline;
var numbers = outline.match(/[-+]?\d*\.?\d+/g);
if (!numbers) return outline;
var values = numbers.map(Number), points = [];
if (values.length < 6 || values.length % 2 || !values.every(isFinite)) return outline;
for (var i = 0; i < values.length; i += 2) {
var p = [values[i], values[i + 1]], last = points[points.length - 1];
if (!last || Math.hypot(p[0] - last[0], p[1] - last[1]) > .001) points.push(p);
}
if (points.length > 1 && Math.hypot(points[0][0] - points[points.length - 1][0], points[0][1] - points[points.length - 1][1]) < .001) points.pop();
if (points.length < 3) return outline;
var corners = points.map(function (p, j) {
var prev = points[(j + points.length - 1) % points.length], next = points[(j + 1) % points.length];
var before = Math.hypot(prev[0] - p[0], prev[1] - p[1]), after = Math.hypot(next[0] - p[0], next[1] - p[1]);
var radius = Math.min(2, before * .35, after * .35);
return { p: p, enter: [p[0] + (prev[0] - p[0]) * radius / before, p[1] + (prev[1] - p[1]) * radius / before],
exit: [p[0] + (next[0] - p[0]) * radius / after, p[1] + (next[1] - p[1]) * radius / after] };
});
function xy(p) { return p.map(function (n) { return n.toFixed(3); }).join(','); }
var path = 'M' + xy(corners[0].exit);
for (var k = 1; k <= corners.length; k++) {
var c = corners[k % corners.length];
path += 'L' + xy(c.enter) + 'Q' + xy(c.p) + ' ' + xy(c.exit);
}
return path + 'Z';
}
/* camera-outline-pure-end */
// The PSD geometry uses the same 375 × 812 coordinate space as the capture mask.
(window.CameraGeometry || []).forEach(function (geometry, index) {
var outline = smoothCameraOutline(geometry.outline);
document.querySelectorAll('#cam-mask path[data-shape="' + index + '"]').forEach(function (path) {
path.setAttribute('d', geometry.outline);
path.setAttribute('d', outline);
});
var group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.setAttribute('data-shape', index);
@@ -107,7 +140,10 @@
}
/* ─── 弹窗管理 ─── */
function openOverlay(id) { $('#' + id).classList.add('show'); }
function openOverlay(id) {
if (id === 'p-privacy' || id === 'p-photoguide') loadAppFont();
$('#' + id).classList.add('show');
}
function closeOverlay(id) { $('#' + id).classList.remove('show'); }
// 点遮罩空白处关闭(与各自关闭按钮同行为:隐私→回启动页、拍照引导→留在档案页)
@@ -581,8 +617,8 @@
var MISS_FRAMES_MAX = 3; // 连续漏检 ≥3 帧(~450ms)才报「未检测到人脸」,容忍单帧抖动
var OK_HOLD_MS = 450; // 条件连续合格时长:滤掉一闪而过的合格帧(防运动模糊),达标立即抓拍
var NO_PROGRESS_MS = 12000; // 单步超过该时长仍未抓拍→浮出调整提示
var PREVIEW_SCALE = 0.85; // 与 CSS #cam-video 的 scale 一致:预览缩小 15%
// 人脸框高/视频高。原标定(取景框≈42% 屏高)为 0.26/0.58;预览缩小 15% 后,同样装满轮廓需要脸在原始帧里大 1/0.85 倍
var PREVIEW_SCALE = 1; // 与 CSS #cam-video 铺满预览一致,镜像只改变方向
// 人脸框高/原始视频高;保留现有距离判定阈值。
var FACE_NEAR = 0.30, FACE_FAR = 0.68;
var CENTER_TOL = 0.18; // 人脸中心偏离取景中线超过屏宽 18% → 提示移入取景框
var LUMA_DARK = 65, LUMA_BRIGHT = 235; // 脸部平均亮度(0–255):过暗/过曝提示调整光线
@@ -611,6 +647,7 @@
}
var detRunning = false, detTimer = null; // 检测循环
var okSince = 0; // 当前连续 ok 起点(0=未在累积)
var readyHapticAt = 0; // 抖动重试时最多每两秒提醒一次
var nudgeTimer = null; // 12s 无进展提示
var capturing = false; // 白屏补光抓帧中(暂停姿态判定)
var lastHint = ''; // 提示文案去重
@@ -636,7 +673,7 @@
$('#cam-mask defs').appendChild(clip);
})();
function setMask(cls) { // '' 静态白框 | 'no-face' 呼吸动画 | 'ok' 品牌绿定格
function setMask(cls) { // State also drives readiness hints; no visible outline stroke.
var m = $('#cam-mask');
m.classList.toggle('no-face', cls === 'no-face');
m.classList.toggle('ok', cls === 'ok');
@@ -664,6 +701,7 @@
// 每步进入/回退时的检测状态机重置(蒙版/提示/无进展计时全部归位)
function resetDetectState() {
okSince = 0;
readyHapticAt = 0;
missFrames = 0;
oppFrames = 0;
lastHint = '';
@@ -737,6 +775,13 @@
} catch (e) { return null; }
}
var missFrames = 0; // 连续漏检帧计数:单帧抖动不报「未检测到人脸」
/* camera-haptic-pure-begin */
function captureHaptic(nav) {
// Unsupported/blocked vibration must never interrupt automatic capture.
try { return typeof nav.vibrate === 'function' && nav.vibrate([70, 40, 70]) !== false; }
catch (e) { return false; }
}
/* camera-haptic-pure-end */
function onDetect(d) {
var hint = '', ok = false;
missFrames = d ? 0 : missFrames + 1;
@@ -784,7 +829,10 @@
// 合格时「保持不动」只显示不播报:随即抓拍,「拍摄完成」会截断它
setHint(ok ? '保持不动' : hint, ok, ok);
if (!ok) { okSince = 0; return; }
if (!okSince) okSince = Date.now();
if (!okSince) {
okSince = Date.now();
if (!readyHapticAt || okSince - readyHapticAt >= 2000) { readyHapticAt = okSince; captureHaptic(navigator); }
}
else if (Date.now() - okSince >= OK_HOLD_MS) { okSince = 0; doCapture(); }
}
@@ -858,12 +906,12 @@
if (!camLive) return;
shutterLock = true;
capturing = true;
if (isManualMode()) captureHaptic(navigator); // Manual shutter has no readiness callback.
clearTimeout(nudgeTimer);
hideNudge();
var seq = camSeq;
var fill = $('#cam-fill');
fill.classList.add('on'); // .on 无过渡,瞬间点亮
if (navigator.vibrate) navigator.vibrate(30);
setTimeout(function () {
var shot = captureFromVideo();
setTimeout(function () {