diff --git a/ROADMAP.md b/ROADMAP.md index 4c6c6f5..6d1d2ba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,6 +31,26 @@ Bericht-Moduls. --- +## 10. Kamera vernünftig + offene Handy-Prüfungen im Emulator (Eddy 19.10.2026: „mach das vernünftig") + +Anlass: Nach der Blitz/Fokus-Gegenprobe (Punkt 9) meldet Chrome außerdem Zoom 1–10× und Belichtungskorrektur ±3. +Der Emulator läuft jetzt (SVM an) — die bisher „nur am Handy" prüfbaren Punkte gehen dort mit echtem Android-Chrome. + +- [x] ✅ 19.10.2026 **Zoom mit zwei Fingern im Sucher** (`applyConstraints({advanced:[{zoom}]})`, nur wenn `caps.zoom` + gemeldet) + Anzeige „1×"/„2,5×" unten im Sucher (gelb, sobald gezoomt), Antippen der Anzeige → zurück auf 1×. + Während der Geste immer nur ein Aufruf an die Kamera, Zwischenwerte verfallen; Ende der Geste löst keinen + Fokus-Tipp aus. Im Emulator mit **echter Zwei-Finger-Geste** (Emulator-Konsole → Touchscreen, siehe unten) geprüft: + Fingerabstand ×2,87 → 2,9×, zusammen → 1×, kein Fokus-Aufruf/Klick während der Gesten; Foto bei 2,5× zeigt genau + den Sucher-Ausschnitt. Chrome 153 zoomt über `control.zoomRatio` (nicht Ausschnitt) — die Fokus-/Belichtungs- + bereiche gelten dann laut AOSP-Definition relativ zum gezoomten Bild, die Tippstelle passt also auch im Zoom. +- [x] ✅ Belichtung: **kein eigener Regler** — Antippen setzt bei Chrome Fokus-, Belichtungs- und Weißabgleich-Bereich + gemeinsam; im Emulator gemessen: `aeRegions` = `afRegions` = `awbRegions` = Tippstelle + → Display/Typenschild antippen = dort scharf UND richtig belichtet. +- [ ] Emulator: echte Kamera + „Fertig" im Funkloch (Fotos gesichert, nach Netz-Rückkehr hochgeladen, Server zählt genau +n) +- [ ] Emulator: Hochladen bei ausgeschaltetem Display (Background Sync) +- [ ] Emulator: 8–10 Fotos auf einmal aus der Galerie (Fix aus Punkt 7) +- [ ] Emulator: Zwei-Finger-Zoom im PDF-Betrachter mit echtem Mehrfinger-Touch (statt CDP-Nachbildung) + ## 8. Knopf „Geteiltes ablegen" bleibt dauerhaft stehen (gemeldet 2026-10-19) Eddy: *„bei der App Baustelle ist permanent ein Button relativ weit unten angezeigt über den aufgelisteten diff --git a/app.css b/app.css index e89590c..128cb4e 100644 --- a/app.css +++ b/app.css @@ -1219,6 +1219,7 @@ body { align-items: center; justify-content: center; overflow: hidden; + touch-action: none; /* zwei Finger zoomen die Kamera, nicht die Seite */ } /* contain: die Vorschau zeigt exakt das, was gespeichert wird (wichtig für Doku-Fotos) */ .camera-modal #cam-video { @@ -1329,6 +1330,24 @@ body { .camera-modal .cam-fokus-ring.aktiv { animation: cam-fokus 0.35s ease-out; } .camera-modal .cam-fokus-ring.ok { border-color: #ffd34d; } @keyframes cam-fokus { from { transform: scale(1.5); opacity: 0.4; } to { transform: scale(1); opacity: 0.9; } } +/* Zoomstufe unten mittig im Sucher; Antippen = zurück auf 1× */ +.camera-modal .cam-zoom { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + min-width: 44px; + height: 32px; + padding: 0 10px; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.5); + background: rgba(0, 0, 0, 0.55); + color: #fff; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} +.camera-modal .cam-zoom.aktiv { color: #ffd34d; border-color: #ffd34d; } .camera-modal [hidden] { display: none !important; } /* ============================================================ diff --git a/app.js b/app.js index e09387e..07f1f3e 100644 --- a/app.js +++ b/app.js @@ -3626,6 +3626,7 @@ async function openCameraModal(orderId) {
Kamera wird gestartet…
+
@@ -3645,6 +3646,7 @@ async function openCameraModal(orderId) { const blitzBtn = modal.querySelector('#cam-blitz'); const fokusRing = modal.querySelector('#cam-fokus-ring'); const flipBtn = modal.querySelector('#cam-flip'); + const zoomBtn = modal.querySelector('#cam-zoom'); let stream = null; let facing = 'environment'; @@ -3791,6 +3793,10 @@ async function openCameraModal(orderId) { * → der Autofokus stellt laufend auf diese Stelle scharf. 'single-shot' meldet Chrome zwar, * loest es auf Android aber nicht ein (kein AF-Trigger in VideoCaptureCamera2.java, der * Modus bleibt CONTINUOUS_PICTURE — im Emulator per dumpsys gemessen, 19.10.2026). + * Chrome setzt mit dem Punkt zugleich Belichtungs- und Weissabgleich-Bereich (AE/AWB_REGIONS): + * Display oder Typenschild antippen = dort auch richtig belichtet, kein eigener Regler noetig. + * - Zoom mit zwei Fingern: track.getCapabilities().zoom, Chrome schneidet den Sensor aus + * (SCALER_CROP_REGION, gilt auch fuer takePhoto). Beim Zoomen verwirft Chrome den Tippbereich. * Frontkamera, Webcam am Tablet (X12) usw. melden meist nichts davon → kein Knopf, kein Antippen. */ const BLITZ_TEXT = { off: '⚡ Aus', auto: '⚡ Auto', flash: '⚡ An', torch: '🔦 Licht' }; @@ -3800,6 +3806,8 @@ async function openCameraModal(orderId) { let blitz = 'off'; let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)? let fokusModus = ''; // 'continuous' | 'single-shot' | '' (kein Antippen) + let zoomBereich = null; // { min, max, step } aus getCapabilities, sonst kein Zoom + let zoom = 1; let tippHinweis = false; function videoTrack() { @@ -3807,8 +3815,8 @@ async function openCameraModal(orderId) { } async function kameraExtras(my) { - imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; - blitzBtn.hidden = true; fokusRing.hidden = true; + imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; zoomBereich = null; zoom = 1; + blitzBtn.hidden = true; fokusRing.hidden = true; zoomBtn.hidden = true; const track = videoTrack(); if (!track) return; let caps = {}; @@ -3841,14 +3849,70 @@ async function openCameraModal(orderId) { const fm = Array.isArray(caps.focusMode) ? caps.focusMode : []; fokusModus = fm.includes('continuous') ? 'continuous' : (fm.includes('single-shot') ? 'single-shot' : ''); - if (fokusModus && !tippHinweis) { + + const zb = caps.zoom; + if (zb && typeof zb.max === 'number' && zb.max > (zb.min || 1)) { + zoomBereich = { min: zb.min || 1, max: zb.max, step: zb.step || 0.1 }; + try { zoom = track.getSettings().zoom || zoomBereich.min; } catch (_) { zoom = zoomBereich.min; } + zoomAnzeigen(); + zoomBtn.hidden = false; + } + + const tipps = []; + if (fokusModus) tipps.push('Antippen stellt dort scharf'); + if (zoomBereich) tipps.push('zwei Finger zoomen'); + if (tipps.length && !tippHinweis) { tippHinweis = true; - hint.textContent = 'Antippen stellt dort scharf'; + const text = tipps.join(' · '); + hint.textContent = text; hint.style.display = ''; - setTimeout(() => { if (hint.textContent === 'Antippen stellt dort scharf') hint.style.display = 'none'; }, 2500); + setTimeout(() => { if (hint.textContent === text) hint.style.display = 'none'; }, 2500); } } + function zoomAnzeigen() { + const z = Math.round(zoom * 10) / 10; + zoomBtn.textContent = (Number.isInteger(z) ? String(z) : z.toFixed(1).replace('.', ',')) + '×'; + zoomBtn.classList.toggle('aktiv', z > (zoomBereich ? zoomBereich.min : 1)); + } + + // Zoom setzen: waehrend der Geste kommen viele Werte — immer nur einen Aufruf gleichzeitig + // an die Kamera schicken, danach den zuletzt gewuenschten (Zwischenwerte verfallen). + let zoomLaeuft = false, zoomZiel = null; + async function zoomSetzen(z) { + if (!zoomBereich) return; + const { min, max, step } = zoomBereich; + zoomZiel = Math.min(max, Math.max(min, Math.round(Math.round(z / step) * step * 100) / 100)); + zoom = zoomZiel; + zoomAnzeigen(); + if (zoomLaeuft) return; + zoomLaeuft = true; + while (zoomZiel !== null) { + const ziel = zoomZiel; zoomZiel = null; + const track = videoTrack(); + if (!track) break; + try { await track.applyConstraints({ advanced: [{ zoom: ziel }] }); } catch (_) {} + } + zoomLaeuft = false; + } + + zoomBtn.onclick = (ev) => { ev.stopPropagation(); if (zoomBereich) zoomSetzen(zoomBereich.min); }; + + // Zwei Finger auf dem Sucher = zoomen (Abstand der Finger relativ zum Start) + let pinch = null, pinchEnde = 0; + const fingerAbstand = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY); + stage.addEventListener('touchstart', (e) => { + if (e.touches.length === 2 && zoomBereich) pinch = { d0: fingerAbstand(e.touches), z0: zoom }; + }, { passive: true }); + stage.addEventListener('touchmove', (e) => { + if (!pinch || e.touches.length !== 2) return; + e.preventDefault(); + zoomSetzen(pinch.z0 * fingerAbstand(e.touches) / pinch.d0); + }, { passive: false }); + const pinchAus = (e) => { if (pinch && (!e.touches || e.touches.length < 2)) { pinch = null; pinchEnde = Date.now(); } }; + stage.addEventListener('touchend', pinchAus); + stage.addEventListener('touchcancel', pinchAus); + async function lichtSchalten(an) { const track = videoTrack(); if (!track) return false; @@ -3893,6 +3957,7 @@ async function openCameraModal(orderId) { stage.addEventListener('click', async (ev) => { const track = videoTrack(); if (!track || !fokusModus || !video.videoWidth) return; + if (pinch || Date.now() - pinchEnde < 400) return; // Ende einer Zoom-Geste ist kein Tipp // Antippstelle in Bildkoordinaten 0..1 — die Vorschau ist eingepasst (object-fit: contain) const r = video.getBoundingClientRect(); const sk = Math.min(r.width / video.videoWidth, r.height / video.videoHeight);