← back to Dw Photo Capture
TK-12162: pre-shot 11-slider adjust panel + auto-save on all simple-* pages
1a2c398e3e088a177d1ff3a9ceb186b554d4aef0 · 2026-09-24 14:17:53 -0700 · Steve Abrams
New public/js/adjust-panel.js puts all 11 CapturePipeline adjustments (exposure,
contrast, highlights, shadows, warmth, tint, saturation, vibrance, hue, sharpness,
straighten) in a bottom sheet, draws the adjusted live preview over the camera, and
bakes the same tune into whichever photo is taken - live shutter or the full-res
native photo (up to 4096px). Settings persist between shots.
Auto-save (default on, switch in the panel): once the label read returns vendor +
mfr#, the draft saves with no extra tap; otherwise the page's manual form shows as
before. Minimal keeps its existing Auto-create switch.
Pro Booth: its 3 big sliders and the panel edit the same tune; straighten was
hard-coded to 0 in its preview and bake, now honoured.
Headless E2E (exposure +60): all 4 pages brighten the saved photo (live ~83->152,
full-res 170->253 at 4032x3024), auto-save fires without a tap, and the manual
save path still works with auto-save off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WCvaA6QQCJp2LMdcXyrUD
Files touched
A public/js/adjust-panel.jsM public/simple-instant.htmlM public/simple-minimal.htmlM public/simple-probooth.htmlM public/simple-wizard.html
Diff
commit 1a2c398e3e088a177d1ff3a9ceb186b554d4aef0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 14:17:53 2026 -0700
TK-12162: pre-shot 11-slider adjust panel + auto-save on all simple-* pages
New public/js/adjust-panel.js puts all 11 CapturePipeline adjustments (exposure,
contrast, highlights, shadows, warmth, tint, saturation, vibrance, hue, sharpness,
straighten) in a bottom sheet, draws the adjusted live preview over the camera, and
bakes the same tune into whichever photo is taken - live shutter or the full-res
native photo (up to 4096px). Settings persist between shots.
Auto-save (default on, switch in the panel): once the label read returns vendor +
mfr#, the draft saves with no extra tap; otherwise the page's manual form shows as
before. Minimal keeps its existing Auto-create switch.
Pro Booth: its 3 big sliders and the panel edit the same tune; straighten was
hard-coded to 0 in its preview and bake, now honoured.
Headless E2E (exposure +60): all 4 pages brighten the saved photo (live ~83->152,
full-res 170->253 at 4032x3024), auto-save fires without a tap, and the manual
save path still works with auto-save off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WCvaA6QQCJp2LMdcXyrUD
---
public/js/adjust-panel.js | 185 ++++++++++++++++++++++++++++++++++++++++++++
public/simple-instant.html | 24 +++++-
public/simple-minimal.html | 21 ++---
public/simple-probooth.html | 17 +++-
public/simple-wizard.html | 22 +++++-
5 files changed, 248 insertions(+), 21 deletions(-)
diff --git a/public/js/adjust-panel.js b/public/js/adjust-panel.js
new file mode 100644
index 0000000..c2f7386
--- /dev/null
+++ b/public/js/adjust-panel.js
@@ -0,0 +1,185 @@
+/*
+ * adjust-panel.js — the pre-shot "Photoshop" panel for the simple-* capture pages (TK-12162).
+ *
+ * All 11 CapturePipeline adjustments as sliders in a bottom sheet, a live adjusted preview drawn
+ * over the camera <video>, and a bake() that applies the SAME tune to the captured photo — so what
+ * you set before the shot is what gets saved. Requires /js/capture-pipeline.js loaded first.
+ *
+ * const panel = AdjustPanel.create({
+ * key, // localStorage key for the tune (ignored when getTune is given)
+ * getTune, // optional: page already owns a tune object (Pro Booth) — panel edits it in place
+ * onChange, // optional: called after any slider move / reset
+ * toggleParent, // optional element to put the "Adjust" button in (default: floating top-left)
+ * toggleClass, toggleText,
+ * showAutoSave // default true — the "Auto-save after label read" switch
+ * });
+ * panel.attachPreview(videoEl) // adjusted canvas drawn over the video (hidden when neutral)
+ * panel.bake(source, maxEdge, {mirror}) // -> JPEG dataUrl (0.92), full tune incl. sharpness
+ * panel.bakeDataUrl(url, maxEdge) // -> Promise<dataUrl>; for the native full-res photo
+ * panel.autoSave() // -> boolean
+ * panel.sync() // re-read the tune into the sliders (after an outside reset)
+ */
+(function () {
+ var CP = window.CapturePipeline;
+ var AUTO_KEY = 'dwSimpleAutoSave';
+ var CONTROLS = [
+ ['exposure', 'Exposure', -100, 100], ['contrast', 'Contrast', -100, 100],
+ ['highlights', 'Highlights', -100, 100], ['shadows', 'Shadows', -100, 100],
+ ['temp', 'Warmth', -100, 100], ['tint', 'Tint', -100, 100],
+ ['saturation', 'Saturation', -100, 100], ['vibrance', 'Vibrance', -100, 100],
+ ['hue', 'Hue', -180, 180], ['sharpness', 'Sharpness', 0, 100], ['straighten', 'Straighten', -15, 15]
+ ];
+ var CSS =
+ '.ap-sheet{position:fixed;left:0;right:0;bottom:0;z-index:9000;max-height:46vh;overflow:auto;' +
+ 'background:rgba(16,14,10,.94);color:#f2ede1;border-top:1px solid #3a3428;border-radius:18px 18px 0 0;' +
+ 'padding:10px 16px calc(14px + env(safe-area-inset-bottom));font:14px/1.3 -apple-system,system-ui,sans-serif;' +
+ 'transform:translateY(105%);transition:transform .22s ease}' +
+ '.ap-sheet.open{transform:none}' +
+ '.ap-head{display:flex;align-items:center;gap:10px;padding:4px 0 8px;position:sticky;top:-10px;background:inherit}' +
+ '.ap-head b{flex:1;font-size:13px;letter-spacing:.16em;text-transform:uppercase;color:#c8a24a}' +
+ '.ap-head button,.ap-toggle-float{border:1px solid #c8a24a;color:#c8a24a;background:rgba(0,0,0,.55);' +
+ 'border-radius:999px;padding:8px 14px;font:600 13px/1 -apple-system,system-ui,sans-serif;cursor:pointer}' +
+ '.ap-row{display:grid;grid-template-columns:92px 1fr 40px;align-items:center;gap:10px;padding:0}' +
+ '.ap-row span{font-size:12px;color:#b3a98f}.ap-row output{text-align:right;color:#e2bd66;font-variant-numeric:tabular-nums}' +
+ '.ap-row input{width:100%;height:28px;accent-color:#c8a24a}' +
+ // host pages style bare button/input globally (Wizard: 70px-tall buttons) — keep the sheet immune
+ '.ap-sheet button{min-width:0!important;min-height:0!important;width:auto;flex:0 0 auto;box-shadow:none;' +
+ 'font:600 13px/1 -apple-system,system-ui,sans-serif!important;padding:8px 14px!important;border-radius:999px!important}' +
+ '.ap-toggle,.ap-toggle-float{white-space:nowrap}' +
+ '.ap-auto{display:flex;align-items:center;gap:10px;padding:10px 0 2px;color:#b3a98f;font-size:13px}' +
+ '.ap-auto input{width:20px;height:20px;accent-color:#c8a24a}' +
+ '.ap-toggle-float{position:fixed;z-index:8999;left:12px;top:calc(env(safe-area-inset-top,0px) + 14px)}' +
+ '.ap-toggle.on,.ap-toggle-float.on{background:#c8a24a;color:#171308}' +
+ '.ap-preview{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none;display:none}';
+
+ function isNeutral(t) {
+ for (var i = 0; i < CONTROLS.length; i++) if (t[CONTROLS[i][0]]) return false;
+ return true;
+ }
+ function readAuto() { try { var v = localStorage.getItem(AUTO_KEY); return v === null ? true : v === '1'; } catch (e) { return true; } }
+ function writeAuto(on) { try { localStorage.setItem(AUTO_KEY, on ? '1' : '0'); } catch (e) {} }
+
+ function create(opts) {
+ opts = opts || {};
+ var ownTune = opts.getTune ? null : CP.loadTune(opts.key || 'dwSimpleTune');
+ var getTune = opts.getTune || function () { return ownTune; };
+ var inputs = {}, outs = {};
+
+ if (!document.getElementById('ap-style')) {
+ var st = document.createElement('style'); st.id = 'ap-style'; st.textContent = CSS; document.head.appendChild(st);
+ }
+ var sheet = document.createElement('div'); sheet.className = 'ap-sheet';
+ var head = document.createElement('div'); head.className = 'ap-head';
+ head.innerHTML = '<b>Adjust before the shot</b>';
+ var resetB = document.createElement('button'); resetB.type = 'button'; resetB.textContent = '↺ Reset';
+ var doneB = document.createElement('button'); doneB.type = 'button'; doneB.textContent = 'Done';
+ head.appendChild(resetB); head.appendChild(doneB); sheet.appendChild(head);
+
+ CONTROLS.forEach(function (c) {
+ var row = document.createElement('label'); row.className = 'ap-row';
+ var name = document.createElement('span'); name.textContent = c[1];
+ var inp = document.createElement('input'); inp.type = 'range'; inp.min = c[2]; inp.max = c[3]; inp.step = 1;
+ var out = document.createElement('output');
+ inp.addEventListener('input', function () {
+ getTune()[c[0]] = parseInt(inp.value, 10) || 0; out.textContent = inp.value; changed();
+ });
+ inp.addEventListener('dblclick', function () { getTune()[c[0]] = 0; sync(); changed(); });
+ row.appendChild(name); row.appendChild(inp); row.appendChild(out); sheet.appendChild(row);
+ inputs[c[0]] = inp; outs[c[0]] = out;
+ });
+
+ var auto = null;
+ if (opts.showAutoSave !== false) {
+ var ar = document.createElement('label'); ar.className = 'ap-auto';
+ auto = document.createElement('input'); auto.type = 'checkbox'; auto.checked = readAuto();
+ auto.addEventListener('change', function () { writeAuto(auto.checked); });
+ ar.appendChild(auto); ar.appendChild(document.createTextNode('Auto-save as a draft once the label is read'));
+ sheet.appendChild(ar);
+ }
+ document.body.appendChild(sheet);
+
+ var toggle = document.createElement('button'); toggle.type = 'button';
+ toggle.textContent = opts.toggleText || '🎚 Adjust';
+ if (opts.toggleParent) { toggle.className = 'ap-toggle ' + (opts.toggleClass || ''); opts.toggleParent.appendChild(toggle); }
+ else { toggle.className = 'ap-toggle-float'; document.body.appendChild(toggle); }
+ toggle.addEventListener('click', function (e) { e.stopPropagation(); sheet.classList.toggle('open'); });
+ doneB.addEventListener('click', function () { sheet.classList.remove('open'); });
+ resetB.addEventListener('click', function () {
+ var t = getTune(); CONTROLS.forEach(function (c) { t[c[0]] = 0; }); sync(); changed();
+ });
+ sheet.addEventListener('click', function (e) { e.stopPropagation(); }); // pages close drawers on outside clicks
+
+ function sync() {
+ var t = getTune();
+ CONTROLS.forEach(function (c) { inputs[c[0]].value = t[c[0]] || 0; outs[c[0]].textContent = t[c[0]] || 0; });
+ toggle.classList.toggle('on', !isNeutral(t));
+ }
+ function changed() {
+ if (ownTune) CP.saveTune(opts.key || 'dwSimpleTune', ownTune);
+ toggle.classList.toggle('on', !isNeutral(getTune()));
+ if (opts.onChange) opts.onChange();
+ }
+ sync();
+
+ // ── live preview: a canvas over the <video>, same apply() as the bake (sharpness is capture-only
+ // by the engine's contract). Hidden while the tune is neutral so the raw video shows. ──
+ var previews = [];
+ function tick() {
+ var t = getTune(), neutral = isNeutral(t);
+ previews.forEach(function (p) {
+ var v = p.video, cv = p.canvas;
+ var show = !neutral && v.videoWidth && v.offsetParent !== null;
+ cv.style.display = show ? 'block' : 'none';
+ if (!show) return;
+ var k = Math.min(1, 640 / Math.max(v.videoWidth, v.videoHeight));
+ var w = Math.round(v.videoWidth * k), h = Math.round(v.videoHeight * k);
+ if (cv.width !== w) cv.width = w; if (cv.height !== h) cv.height = h;
+ cv.className = 'ap-preview ' + v.className; // carries e.g. the selfie .mirror transform
+ try {
+ var x = cv.getContext('2d', { willReadFrequently: true });
+ CP.drawSource(x, v, w, h, t.straighten);
+ CP.apply(x, w, h, t, { capture: false });
+ } catch (e) {}
+ });
+ }
+ var timer = null;
+ function attachPreview(video) {
+ var cv = document.createElement('canvas'); cv.className = 'ap-preview';
+ if (getComputedStyle(video.parentNode).position === 'static') video.parentNode.style.position = 'relative';
+ video.parentNode.insertBefore(cv, video.nextSibling);
+ previews.push({ video: video, canvas: cv });
+ if (!timer) timer = setInterval(tick, 80);
+ }
+
+ function bake(source, maxEdge, bo) {
+ var sw = source.videoWidth || source.naturalWidth || source.width;
+ var sh = source.videoHeight || source.naturalHeight || source.height;
+ var k = Math.min(1, (maxEdge || 2000) / Math.max(sw, sh));
+ var w = Math.round(sw * k), h = Math.round(sh * k), t = getTune();
+ var c = document.createElement('canvas'); c.width = w; c.height = h;
+ var x = c.getContext('2d', { willReadFrequently: true });
+ if (bo && bo.mirror) x.setTransform(-1, 0, 0, 1, w, 0);
+ CP.drawSource(x, source, w, h, t.straighten);
+ x.setTransform(1, 0, 0, 1, 0, 0);
+ if (!isNeutral(t)) CP.apply(x, w, h, t, { capture: true });
+ return c.toDataURL('image/jpeg', 0.92);
+ }
+ function bakeDataUrl(url, maxEdge) {
+ if (isNeutral(getTune())) return Promise.resolve(url);
+ return new Promise(function (res) {
+ var im = new Image();
+ im.onload = function () { try { res(bake(im, maxEdge)); } catch (e) { console.error('[adjust-panel] bake failed', e); res(url); } };
+ im.onerror = function () { res(url); };
+ im.src = url;
+ });
+ }
+
+ return {
+ attachPreview: attachPreview, bake: bake, bakeDataUrl: bakeDataUrl, sync: sync,
+ autoSave: function () { return auto ? auto.checked : false; },
+ toggle: toggle, tune: getTune, isNeutral: function () { return isNeutral(getTune()); }
+ };
+ }
+
+ window.AdjustPanel = { create: create, CONTROLS: CONTROLS };
+})();
diff --git a/public/simple-instant.html b/public/simple-instant.html
index c761f6a..588e821 100644
--- a/public/simple-instant.html
+++ b/public/simple-instant.html
@@ -248,6 +248,8 @@
<script src="/js/field-clean.js"></script>
<script src="/js/native-photo.js"></script>
+<script src="/js/capture-pipeline.js"></script>
+<script src="/js/adjust-panel.js"></script>
<script>
(function(){
var $ = function(id){ return document.getElementById(id); };
@@ -265,6 +267,16 @@
var busy = false, capturedDataUrl = null, extractDone = false, extractAbort = null;
var vendorFromExtract = false, sessCount = 0;
+ // Pre-shot adjustments (all 11) + auto-save once the label read fills vendor + mfr#.
+ var panel = AdjustPanel.create({ key:'dwSimpleTune', toggleParent: $('fullResBtn').parentNode, toggleClass:'fullres-btn' });
+ panel.attachPreview(video);
+ var autoFired = false;
+ function maybeAutoSave(){
+ if (autoFired || !panel.autoSave() || !extractDone || chinFields.hidden || saveBtn.disabled) return;
+ if (!fVendor.value.trim() || !fMfr.value.trim()) return;
+ autoFired = true; saveBtn.click();
+ }
+
function toast(msg, ms){
toastEl.textContent = msg; toastEl.classList.add('show');
clearTimeout(toast._t); toast._t = setTimeout(function(){ toastEl.classList.remove('show'); }, ms || 2600);
@@ -324,7 +336,7 @@
async function doCapture(native){
if (busy || (!stream && !native)) return;
- busy = true; shutterBtn.disabled = true;
+ busy = true; shutterBtn.disabled = true; autoFired = false;
if (native) {
capturedDataUrl = native.full;
@@ -333,8 +345,7 @@
var vw = video.videoWidth || 1280, vh = video.videoHeight || 960;
var maxDim = 2000, scale = Math.min(1, maxDim / Math.max(vw, vh));
canvas.width = Math.round(vw * scale); canvas.height = Math.round(vh * scale);
- canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
- capturedDataUrl = canvas.toDataURL('image/jpeg', 0.92);
+ capturedDataUrl = panel.bake(video, maxDim); // pre-shot adjustments baked in
}
// ── SPEED: fire the REAL network call the instant we have pixels — before any
@@ -376,12 +387,14 @@
else if (data && data.ok === false) tinyMeta.innerHTML = '🔎 checked in <b>' + fmt(elapsed) + '</b> · enter details by hand';
else tinyMeta.innerHTML = '🔎 label read in <b>' + fmt(elapsed) + '</b>';
updateSaveEnabled();
+ maybeAutoSave();
});
}
function revealResult(){
developingRow.hidden = true; chinFields.hidden = false;
updateSaveEnabled();
+ maybeAutoSave();
if (!extractDone) tinyMeta.innerHTML = '🔎 still reading the label…';
fVendor.focus({ preventScroll:true });
}
@@ -390,7 +403,10 @@
// Full-res: the phone's own Camera app (real sensor photo); a 1600px copy feeds the label read.
$('fullResBtn').addEventListener('click', function(){
if (busy) return;
- NativePhoto.pick().then(function(p){ if (p) doCapture(p); });
+ NativePhoto.pick().then(function(p){
+ if (!p) return;
+ panel.bakeDataUrl(p.full, NativePhoto.FULL_EDGE).then(function(full){ doCapture({ full: full, small: p.small }); });
+ });
});
retakeBtn.addEventListener('click', function(){
diff --git a/public/simple-minimal.html b/public/simple-minimal.html
index 5523fc5..48b5d82 100644
--- a/public/simple-minimal.html
+++ b/public/simple-minimal.html
@@ -36,6 +36,8 @@
<title>DW Photo — Minimal Showroom</title>
<script src="/js/field-clean.js"></script>
<script src="/js/native-photo.js"></script>
+<script src="/js/capture-pipeline.js"></script>
+<script src="/js/adjust-panel.js"></script>
<style>
:root{ --bg:#0f0e0c; --panel:#1b1710; --card:#1b1916; --ink:#f3efe7; --muted:#9a9184;
--line:#2e2a25; --gold:#c8a24a; --green:#3fa06a; --red:#c0563f; }
@@ -211,7 +213,10 @@
var swAuto = $('swAuto'), swTiming = $('swTiming');
// ── state ────────────────────────────────────────────────────────────────
- var STATE = 'camera'; // camera | processing | result
+ var STATE = 'camera';
+ // pre-shot adjustments (all 11) — Minimal keeps its own Auto-create switch, so no second one here
+ var panel = AdjustPanel.create({ key:'dwSimpleTune', showAutoSave:false });
+ panel.attachPreview(camEl); // camera | processing | result
var stream = null, facing = 'environment';
var AUTO_CREATE = true, SHOW_TIMING = false;
var VENDORS = []; // warmed speculatively, used only for the manual fallback
@@ -299,14 +304,8 @@
// Another" needs no getUserMedia round trip either — just hide the frozen frame. ────────
var shotCanvas = document.createElement('canvas');
function captureFrame(){
- var vw = camEl.videoWidth || 1280, vh = camEl.videoHeight || 960;
- var k = Math.min(1, 2000 / Math.max(vw, vh)); // same 2000px max edge as Pro Booth
- var w = Math.round(vw * k), h = Math.round(vh * k);
- shotCanvas.width = w; shotCanvas.height = h;
- var ctx = shotCanvas.getContext('2d');
- if (facing === 'user') { ctx.translate(w,0); ctx.scale(-1,1); }
- ctx.drawImage(camEl, 0, 0, w, h);
- return shotCanvas.toDataURL('image/jpeg', 0.92);
+ // 2000px max edge (same as Pro Booth), with the pre-shot adjustments baked in
+ return panel.bake(camEl, 2000, { mirror: facing === 'user' });
}
var tStart = 0;
@@ -328,13 +327,15 @@
if (STATE !== 'camera') return;
NativePhoto.pick().then(function(p){
if (!p) { return; }
+ return panel.bakeDataUrl(p.full, NativePhoto.FULL_EDGE).then(function(full){
if (STATE !== 'camera') return;
tStart = performance.now();
- capturedDataUrl = p.full;
+ capturedDataUrl = full;
shotEl.src = capturedDataUrl;
shotEl.classList.add('on');
toProcessing('Reading label…');
runPipeline(capturedDataUrl, p.small);
+ });
});
});
diff --git a/public/simple-probooth.html b/public/simple-probooth.html
index 00cbb8f..391f056 100644
--- a/public/simple-probooth.html
+++ b/public/simple-probooth.html
@@ -66,6 +66,7 @@
.bar .sp{flex:1}
.chip{border:1px solid var(--line);border-radius:999px;padding:7px 14px;font-size:12.5px;color:var(--dim);background:rgba(0,0,0,.35)}
.chip:active{color:var(--ink);border-color:var(--gold)}
+ .bar .chip{white-space:nowrap}
/* ── LIVE ── */
#live{background:#000}
@@ -247,6 +248,7 @@
<script src="/js/acquire-camera.js"></script>
<script src="/js/field-clean.js"></script>
<script src="/js/native-photo.js"></script>
+<script src="/js/adjust-panel.js"></script>
<script>
'use strict';
/* Pro Booth client — see the header comment for the speed architecture. */
@@ -267,18 +269,22 @@ function bindSliders(){
el.addEventListener('input',()=>{
tune[key]=parseInt(el.value,10)||0;
$('#'+out).textContent=tune[key];
- CapturePipeline.saveTune(TUNE_KEY,tune);
+ CapturePipeline.saveTune(TUNE_KEY,tune); panel.sync();
hwNudge();
});
}
$('#resetBtn').addEventListener('click',()=>{
tune=CapturePipeline.defaultTune(); CapturePipeline.saveTune(TUNE_KEY,tune);
- syncSliders(); hwNudge(); toast('Adjustments reset');
+ syncSliders(); panel.sync(); hwNudge(); toast('Adjustments reset');
});
}
/* best-effort hardware assist (never authoritative — the software bake owns WYSIWYG) */
let hwT=null;
function hwNudge(){ clearTimeout(hwT); hwT=setTimeout(()=>{ try{ const tr=acq.getTrack(); if(tr) CapturePipeline.Hardware.apply(tr,tune); }catch(e){} },250); }
+/* all 11 adjustments: the panel edits the SAME tune object the 3 big sliders, preview and bake use */
+const panel = AdjustPanel.create({ getTune:()=>tune, toggleText:'🎚 All 11', toggleParent: document.querySelector('#live .bar'), toggleClass:'chip',
+ onChange:()=>{ CapturePipeline.saveTune(TUNE_KEY,tune); syncSliders(); hwNudge(); } });
+$('#resetBtn').before(panel.toggle);
/* ── camera (shared hardened acquirer — bounded getUserMedia + play, generation-guarded) ── */
const acq = AcquireCamera.createCameraAcquirer();
@@ -328,7 +334,7 @@ function previewTick(){
const pv=$('#pv'); if(pv.width!==pw)pv.width=pw; if(pv.height!==ph)pv.height=ph;
const x=pv.getContext('2d',{willReadFrequently:true});
try{
- CapturePipeline.drawSource(x,v,pw,ph,0);
+ CapturePipeline.drawSource(x,v,pw,ph,tune.straighten);
CapturePipeline.apply(x,pw,ph,tune,{capture:false});
}catch(e){}
}
@@ -346,7 +352,7 @@ function bakeFrom(source, maxEdge){
const ctx=c.getContext('2d',{willReadFrequently:true});
const t0=performance.now();
try{
- CapturePipeline.drawSource(ctx,source,w,h,0);
+ CapturePipeline.drawSource(ctx,source,w,h,tune.straighten);
CapturePipeline.apply(ctx,w,h,tune,{capture:true}); // ← the ONE full-res bake
}catch(e){ return null; }
const url=c.toDataURL('image/jpeg',0.92);
@@ -382,6 +388,9 @@ function fireExtract(url){
extracted=f;
st.textContent='✓ Label read — check the fields, then Save.'; st.classList.add('on');
showTiming();
+ if(panel.autoSave() && $('#fVendor').value.trim() && $('#fMfr').value.trim() && !$('#saveBtn').disabled){
+ st.textContent='✓ Label read — saving draft…'; $('#saveBtn').click();
+ }
});
}
let extracted={};
diff --git a/public/simple-wizard.html b/public/simple-wizard.html
index 11bc255..7011333 100644
--- a/public/simple-wizard.html
+++ b/public/simple-wizard.html
@@ -11,6 +11,8 @@
<link rel="icon" href="/icon-192.png">
<script src="/js/field-clean.js"></script>
<script src="/js/native-photo.js"></script>
+<script src="/js/capture-pipeline.js"></script>
+<script src="/js/adjust-panel.js"></script>
<style>
/* ============================================================================================
GUIDED WIZARD — TK-12162 concept-guided-wizard
@@ -352,9 +354,17 @@
// ---------------------------------------------------------------------------------------------
// STEP 1 → 2 : "I'm Ready"
// ---------------------------------------------------------------------------------------------
+ // Pre-shot adjustments (all 11): live on both camera views, baked into whichever photo is taken.
+ const panel = AdjustPanel.create({ key:'dwSimpleTune', toggleParent: document.querySelector('#screen-1 .actions'), toggleClass:'btn-secondary' });
+ panel.attachPreview($('video')); panel.attachPreview($('video2'));
+
// Full-res: the phone's own Camera app (real sensor photo). Works even if the live camera failed.
$('btnNative').addEventListener('click', () => {
- NativePhoto.pick().then(p => { if (!p) return; mark('captureTap'); grabFrame(p); });
+ NativePhoto.pick().then(p => {
+ if (!p) return;
+ mark('captureTap');
+ panel.bakeDataUrl(p.full, NativePhoto.FULL_EDGE).then(full => grabFrame({ full, small: p.small }));
+ });
});
$('btnReady').addEventListener('click', () => {
@@ -400,8 +410,7 @@
const vw = v.videoWidth || 1280, vh = v.videoHeight || 960;
const scale = Math.min(1, MAXW / Math.max(vw, vh));
canvas.width = Math.round(vw * scale); canvas.height = Math.round(vh * scale);
- canvas.getContext('2d').drawImage(v, 0, 0, canvas.width, canvas.height);
- capturedDataUrl = canvas.toDataURL('image/jpeg', 0.92);
+ capturedDataUrl = panel.bake(v, MAXW); // pre-shot adjustments baked in
}
mark('frameGrabbed');
@@ -416,6 +425,7 @@
// already done, so only ONE round trip (/api/create-item) remains on the critical path
// instead of two SEQUENTIAL ones. This mirrors Jev's "one request per decision, no idle
// network gaps" — we just move the decision point earlier because we can predict it.
+ const shotRef = capturedDataUrl;
$('analyzingPill').classList.add('show');
mark('extractFired');
extractPromise = fetch('/api/extract', {
@@ -425,6 +435,12 @@
mark('extractResolved');
extractResult = j;
$('analyzingPill').classList.remove('show');
+ // auto-save: label read gave vendor + mfr# -> confirm for the guest (same path as the Use tap)
+ const jf = (j && j.fields) || {};
+ if (panel.autoSave() && capturedDataUrl === shotRef && currentStep === 3 && j && j.ok !== false
+ && (j.vendor_matched || cleanField(jf.vendor)) && cleanField(jf.mfr_sku) && !$('btnUse').disabled) {
+ setTimeout(() => { if (currentStep === 3 && capturedDataUrl === shotRef) $('btnUse').click(); }, 0);
+ }
return j;
}).catch(e => {
mark('extractResolved');
← 4d1311b TK-12162: full-res native-camera capture on all simple-* pag
·
back to Dw Photo Capture
·
TK-12090: verify script resolves dwphoto by name, not stale 7e1353c →