Kamera: Zoom mit zwei Fingern, Antippen belichtet auch dort [deploy]
- Zwei Finger im Sucher zoomen (nur wenn die Kamera Zoom meldet), Anzeige „1×"/„2,5×" unten im Sucher, Antippen der Anzeige setzt auf 1× zurück. Während der Geste geht immer nur ein Aufruf an die Kamera; das Ende der Geste zählt nicht als Fokus-Tipp. - Hinweis beim Öffnen: „Antippen stellt dort scharf · zwei Finger zoomen". - Kein eigener Helligkeitsregler: Chrome setzt mit der Tippstelle auch Belichtung und Weißabgleich (im Emulator gemessen). Im Android-Emulator mit echter Zwei-Finger-Geste geprüft; das Foto bei 2,5× zeigt genau den Sucher-Ausschnitt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7c8a3c4bc0
commit
3028ff5731
3 changed files with 109 additions and 5 deletions
20
ROADMAP.md
20
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)
|
## 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
|
Eddy: *„bei der App Baustelle ist permanent ein Button relativ weit unten angezeigt über den aufgelisteten
|
||||||
|
|
|
||||||
19
app.css
19
app.css
|
|
@ -1219,6 +1219,7 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
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) */
|
/* contain: die Vorschau zeigt exakt das, was gespeichert wird (wichtig für Doku-Fotos) */
|
||||||
.camera-modal #cam-video {
|
.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.aktiv { animation: cam-fokus 0.35s ease-out; }
|
||||||
.camera-modal .cam-fokus-ring.ok { border-color: #ffd34d; }
|
.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; } }
|
@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; }
|
.camera-modal [hidden] { display: none !important; }
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|
|
||||||
75
app.js
75
app.js
|
|
@ -3626,6 +3626,7 @@ async function openCameraModal(orderId) {
|
||||||
<video id="cam-video" playsinline autoplay muted></video>
|
<video id="cam-video" playsinline autoplay muted></video>
|
||||||
<div class="cam-hint" id="cam-hint">Kamera wird gestartet…</div>
|
<div class="cam-hint" id="cam-hint">Kamera wird gestartet…</div>
|
||||||
<div class="cam-fokus-ring" id="cam-fokus-ring" hidden></div>
|
<div class="cam-fokus-ring" id="cam-fokus-ring" hidden></div>
|
||||||
|
<button class="cam-zoom" id="cam-zoom" title="Zoom zurück auf 1×" hidden>1×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cam-filmstrip" id="cam-strip"></div>
|
<div class="cam-filmstrip" id="cam-strip"></div>
|
||||||
<div class="cam-controls">
|
<div class="cam-controls">
|
||||||
|
|
@ -3645,6 +3646,7 @@ async function openCameraModal(orderId) {
|
||||||
const blitzBtn = modal.querySelector('#cam-blitz');
|
const blitzBtn = modal.querySelector('#cam-blitz');
|
||||||
const fokusRing = modal.querySelector('#cam-fokus-ring');
|
const fokusRing = modal.querySelector('#cam-fokus-ring');
|
||||||
const flipBtn = modal.querySelector('#cam-flip');
|
const flipBtn = modal.querySelector('#cam-flip');
|
||||||
|
const zoomBtn = modal.querySelector('#cam-zoom');
|
||||||
|
|
||||||
let stream = null;
|
let stream = null;
|
||||||
let facing = 'environment';
|
let facing = 'environment';
|
||||||
|
|
@ -3791,6 +3793,10 @@ async function openCameraModal(orderId) {
|
||||||
* → der Autofokus stellt laufend auf diese Stelle scharf. 'single-shot' meldet Chrome zwar,
|
* → 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
|
* 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).
|
* 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.
|
* 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' };
|
const BLITZ_TEXT = { off: '⚡ Aus', auto: '⚡ Auto', flash: '⚡ An', torch: '🔦 Licht' };
|
||||||
|
|
@ -3800,6 +3806,8 @@ async function openCameraModal(orderId) {
|
||||||
let blitz = 'off';
|
let blitz = 'off';
|
||||||
let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)?
|
let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)?
|
||||||
let fokusModus = ''; // 'continuous' | 'single-shot' | '' (kein Antippen)
|
let fokusModus = ''; // 'continuous' | 'single-shot' | '' (kein Antippen)
|
||||||
|
let zoomBereich = null; // { min, max, step } aus getCapabilities, sonst kein Zoom
|
||||||
|
let zoom = 1;
|
||||||
let tippHinweis = false;
|
let tippHinweis = false;
|
||||||
|
|
||||||
function videoTrack() {
|
function videoTrack() {
|
||||||
|
|
@ -3807,8 +3815,8 @@ async function openCameraModal(orderId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function kameraExtras(my) {
|
async function kameraExtras(my) {
|
||||||
imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = '';
|
imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; zoomBereich = null; zoom = 1;
|
||||||
blitzBtn.hidden = true; fokusRing.hidden = true;
|
blitzBtn.hidden = true; fokusRing.hidden = true; zoomBtn.hidden = true;
|
||||||
const track = videoTrack();
|
const track = videoTrack();
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
let caps = {};
|
let caps = {};
|
||||||
|
|
@ -3841,14 +3849,70 @@ async function openCameraModal(orderId) {
|
||||||
|
|
||||||
const fm = Array.isArray(caps.focusMode) ? caps.focusMode : [];
|
const fm = Array.isArray(caps.focusMode) ? caps.focusMode : [];
|
||||||
fokusModus = fm.includes('continuous') ? 'continuous' : (fm.includes('single-shot') ? 'single-shot' : '');
|
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;
|
tippHinweis = true;
|
||||||
hint.textContent = 'Antippen stellt dort scharf';
|
const text = tipps.join(' · ');
|
||||||
|
hint.textContent = text;
|
||||||
hint.style.display = '';
|
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) {
|
async function lichtSchalten(an) {
|
||||||
const track = videoTrack();
|
const track = videoTrack();
|
||||||
if (!track) return false;
|
if (!track) return false;
|
||||||
|
|
@ -3893,6 +3957,7 @@ async function openCameraModal(orderId) {
|
||||||
stage.addEventListener('click', async (ev) => {
|
stage.addEventListener('click', async (ev) => {
|
||||||
const track = videoTrack();
|
const track = videoTrack();
|
||||||
if (!track || !fokusModus || !video.videoWidth) return;
|
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)
|
// Antippstelle in Bildkoordinaten 0..1 — die Vorschau ist eingepasst (object-fit: contain)
|
||||||
const r = video.getBoundingClientRect();
|
const r = video.getBoundingClientRect();
|
||||||
const sk = Math.min(r.width / video.videoWidth, r.height / video.videoHeight);
|
const sk = Math.min(r.width / video.videoWidth, r.height / video.videoHeight);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue