Camera selector, focus controls, and decode improvements

- Camera picker with auto-selection of main back camera (index 0)
- Manual/auto focus toggle button with focus distance slider
- Dual binarizer (Hybrid + GlobalHistogram) for blur tolerance
- Only vibrate on new unique scan result
- Band canvas to restrict decoding to aim line region
- Camera select overlaid in viewfinder corner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 23:53:20 +00:00
parent 730cbf5b1b
commit 2fbe6f4334
2 changed files with 48 additions and 14 deletions

23
app.mjs
View File

@@ -11,7 +11,8 @@ const cam_info = document.getElementById('cam-info');
const focus_row = document.getElementById('focus-row'); const focus_row = document.getElementById('focus-row');
const focus_slider = document.getElementById('focus-slider'); const focus_slider = document.getElementById('focus-slider');
const focus_val_el = document.getElementById('focus-val'); const focus_val_el = document.getElementById('focus-val');
const focus_auto = document.getElementById('focus-auto'); const focus_auto_btn = document.getElementById('focus-auto-btn');
let focus_auto = true;
// Canvases for ZXing decode pipeline // Canvases for ZXing decode pipeline
const strip_canvas = document.createElement('canvas'); const strip_canvas = document.createElement('canvas');
const strip_ctx = strip_canvas.getContext('2d', { willReadFrequently: true }); const strip_ctx = strip_canvas.getContext('2d', { willReadFrequently: true });
@@ -76,11 +77,18 @@ async function start_camera(device_id = null) {
} }
if (!has_continuous) { if (!has_continuous) {
focus_auto.checked = false; focus_auto = false;
focus_auto.disabled = true; focus_auto_btn.disabled = true;
} else {
focus_auto_btn.disabled = false;
} }
focus_auto_btn.classList.toggle('active', focus_auto);
focus_auto.onchange = () => apply_focus(track, has_continuous, has_distance); focus_auto_btn.onclick = async () => {
focus_auto = !focus_auto;
focus_auto_btn.classList.toggle('active', focus_auto);
focus_slider.disabled = focus_auto;
await apply_focus(active_track, has_continuous, has_distance);
};
await apply_focus(track, has_continuous, has_distance); await apply_focus(track, has_continuous, has_distance);
// Tap to focus (replace listener on camera switch) // Tap to focus (replace listener on camera switch)
@@ -98,8 +106,7 @@ async function start_camera(device_id = null) {
// --- Focus --- // --- Focus ---
async function apply_focus(track, has_continuous, has_distance) { async function apply_focus(track, has_continuous, has_distance) {
const auto = focus_auto.checked; const auto = focus_auto;
focus_slider.disabled = auto;
try { try {
if (auto && has_continuous) { if (auto && has_continuous) {
await track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }); await track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] });
@@ -276,9 +283,7 @@ function attempt_decode() {
band_canvas.width = strip_canvas.width; band_canvas.width = strip_canvas.width;
band_canvas.height = band_h; band_canvas.height = band_h;
} }
band_ctx.filter = 'contrast(1.8) brightness(1.05)';
band_ctx.drawImage(strip_canvas, 0, band_y, strip_canvas.width, band_h, 0, 0, strip_canvas.width, band_h); band_ctx.drawImage(strip_canvas, 0, band_y, strip_canvas.width, band_h, 0, 0, strip_canvas.width, band_h);
band_ctx.filter = 'none';
decode_attempts++; decode_attempts++;
const source = new ZXing.HTMLCanvasElementLuminanceSource(band_canvas); const source = new ZXing.HTMLCanvasElementLuminanceSource(band_canvas);

View File

@@ -55,6 +55,20 @@
height: 100%; height: 100%;
} }
#camera-select {
position: absolute;
bottom: 8px;
right: 8px;
max-width: 60%;
padding: 4px 6px;
background: rgba(0,0,0,0.6);
color: #dce4ef;
border: 1px solid #1e2a38;
border-radius: 6px;
font-family: inherit;
font-size: 11px;
}
#debug { #debug {
position: absolute; position: absolute;
top: 6px; top: 6px;
@@ -117,6 +131,24 @@
font-size: 13px; font-size: 13px;
} }
#focus-auto-btn {
padding: 8px 14px;
border-radius: 6px;
border: 1px solid #1e2a38;
background: #181e28;
color: var(--dim);
font-family: inherit;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
}
#focus-auto-btn.active {
background: #0d1f15;
border-color: var(--accent);
color: var(--accent);
}
#focus-slider { #focus-slider {
flex: 1; flex: 1;
accent-color: var(--accent); accent-color: var(--accent);
@@ -147,19 +179,16 @@
<video id="video" autoplay playsinline muted></video> <video id="video" autoplay playsinline muted></video>
<canvas id="overlay"></canvas> <canvas id="overlay"></canvas>
<div id="debug"></div> <div id="debug"></div>
<select id="camera-select" style="display:none;"></select>
</div> </div>
<div id="bottom-panel"> <div id="bottom-panel">
<div id="cam-info" style="width:100%;font-size:11px;color:#5a6475;word-break:break-all;"></div> <div id="cam-info" style="width:100%;font-size:11px;color:#5a6475;word-break:break-all;"></div>
<div id="result">Point camera at a barcode</div> <div id="result">Point camera at a barcode</div>
<div id="focus-row"> <div id="focus-row">
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;"> <button id="focus-auto-btn">Auto</button>
<input id="focus-auto" type="checkbox" checked>
Auto
</label>
<input id="focus-slider" type="range" min="0" max="1.62" step="0.01" value="0.3" disabled> <input id="focus-slider" type="range" min="0" max="1.62" step="0.01" value="0.3" disabled>
<span id="focus-val">0.30m</span> <span id="focus-val">0.30m</span>
</div> </div>
<select id="camera-select" style="display:none; width:100%; padding:8px; background:#181e28; color:#dce4ef; border:1px solid #1e2a38; border-radius:8px; font-family:inherit;"></select>
<button id="start-btn">Start Camera</button> <button id="start-btn">Start Camera</button>
</div> </div>
</div> </div>