fix(h5): optimize camera flipping and angle haptics, refresh splash branding

This commit is contained in:
xsl
2026-10-13 17:57:55 +08:00
parent e71368db6d
commit 7381306139
19 changed files with 2125 additions and 1282 deletions
+42 -10
View File
@@ -36,8 +36,8 @@ assert.equal(JSON.stringify(geometry.window.CameraGeometry), original);
for (const value of [null, '', 'M--Z', 'M1,2L1,2Z', 'M0,0L0,0L0,0Z', 'M0,0Q1,2 3,4Z']) assert.equal(context.smoothCameraOutline(value), value);
for (const nav of [null, {}, { vibrate: false }, { vibrate: () => false }, { vibrate: () => { throw Error('denied'); } }]) assert.equal(context.captureHaptic(nav), false);
let pattern;
assert(context.captureHaptic({ vibrate: p => { pattern = Array.from(p); return true; } }));
assert.deepEqual(pattern, [70, 40, 70]);
assert(context.captureHaptic({ vibrate: p => { pattern = p; return true; } }));
assert.equal(pattern, 30);
// Exercise the actual readiness state machine; only camera/pose inputs and clock
// are stubs; the relaxed shared hold policy now requires 450ms/2 frames.
@@ -47,7 +47,8 @@ const state = {
faceOffsetX: () => 0, CENTER_TOL: .18, CAM_STEPS: [{ target: 'front' }], camStep: 0,
HeadPose: { check: () => ({ ok: true }) }, sideYawFlip: false, faceLuma: () => 100,
LUMA_DARK: 65, LUMA_BRIGHT: 235, window: {}, setMask: () => {}, setHint: () => {},
okSince: 0, readyHapticAt: 0, OK_HOLD_MS: 450, Date: { now: () => now },
okSince: 0, OK_HOLD_MS: 450, Date: { now: () => now },
shutterLock: false, capturing: false, camBusy: false, camLive: true, currentScreen: 's-cam', document: { hidden: false },
holdState: null, CaptureQuality: {
evaluate: d => ({ ok: d.detection.box.height >= 100, hint: '保持不动', observation: { yaw: 0, pitch: 0, center_x: .5, center_y: .5, ratio: .5 } }),
hold: require('../js/capture-quality').hold
@@ -56,13 +57,13 @@ const state = {
};
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function onDetect(d)'), source.indexOf(' /* ─── 拍照流程')), state);
const face = { detection: { box: { height: 250 } }, landmarks: { positions: [] } };
state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
now += 200; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
now += 250; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 1);
state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 0);
now += 200; state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 0);
now += 250; state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 1);
now += 100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
now += 100; state.onDetect(face); assert.equal(buzzes, 1, 'jitter must not repeatedly buzz');
now += 100; state.onDetect(face); assert.equal(buzzes, 0, 'good angles must not buzz');
now += 2100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
state.onDetect(face); assert.equal(buzzes, 2);
state.onDetect(face); assert.equal(buzzes, 0);
state.navigator.vibrate = () => { throw Error('blocked'); };
now += 300; state.onDetect(face);
now += 150; state.onDetect(face); assert.equal(captures, 2, 'vibration failure must not prevent capture');
@@ -74,8 +75,39 @@ for (const manual of [true, false]) {
$: () => ({ classList: { add: () => {} } }), setTimeout: (_, ms) => scheduled.push(ms) };
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function doCapture()'), source.indexOf(' // 三张拍完:按视角重排')), shutter);
shutter.doCapture();
assert.equal(manualBuzzes, manual ? 1 : 0, 'auto must not double buzz; manual must retain its cue');
assert.equal(manualBuzzes, 0, 'neither auto nor manual shutter should buzz');
assert(shutter.shutterLock && shutter.capturing);
assert.deepEqual(scheduled, [300], 'blocked haptic must still schedule camera capture');
}
console.log('PASS: three bounded rounded contours; haptic safety, pre-capture cue, jitter throttling; relaxed 450ms/2-frame hold');
// Drive actual onDetect with angle-only failures, not UI hint string matching.
const events = [];
state.navigator.vibrate = p => { events.push({ p, at: now }); return true; };
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle', hint: '请正对屏幕' });
state.onDetect(face);
now += 999; state.onDetect(face); assert.equal(events.length, 0);
now += 1; state.onDetect(face); assert.equal(events.length, 1); assert.equal(events[0].p, 30);
for (let i = 0; i < 3; i++) { now += 1000; state.onDetect(face); }
assert.equal(events.length, 1, 'no repeat before 4 seconds');
now += 1000; state.onDetect(face); assert.equal(events.length, 2);
state.onDetect(null); assert.equal(events.at(-1).p, 0, 'missing face cancels vibration immediately');
const positives = () => events.filter(e => e.p > 0).length;
state.onDetect(face); now += 999; state.onDetect(face); assert.equal(positives(), 2, 'new continuous interval required');
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'quality', hint: '请靠近一点' });
now += 10000; state.onDetect(face); assert.equal(positives(), 2, 'distance/lighting failure does not buzz');
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle', hint: '请正对屏幕' });
state.onDetect(face); now += 1000; state.onDetect(face); assert.equal(positives(), 3);
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'quality', hint: '请到光线充足处' });
state.onDetect(face); assert.equal(events.at(-1).p, 0, 'valid angle cancels even when lighting fails');
for (const [key, value] of [['camBusy', true], ['shutterLock', true], ['capturing', true], ['camLive', false], ['currentScreen', 's-report']]) {
const previous = state[key]; state[key] = value;
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle' });
state.onDetect(face); now += 5000; state.onDetect(face);
assert.equal(positives(), 3, key + ' blocks reminders'); state[key] = previous;
}
state.document.hidden = true; state.onDetect(face); now += 5000; state.onDetect(face); assert.equal(positives(), 3);
state.document.hidden = false;
for (const nav of [{}, { vibrate: () => false }, { vibrate: () => { throw Error('blocked'); } }]) {
state.navigator = nav; state.resetAngleHaptic();
state.onDetect(face); now += 1000; assert.doesNotThrow(() => state.onDetect(face));
}
console.log('PASS: rounded contours; 1s invalid-angle cue, 4s throttle, cancellation/lifecycle, unsupported safety; unchanged 450ms/2-frame capture hold');