Kamera: Antippen stellt an der richtigen Stelle scharf, Wechselknopf als Symbol [deploy]

Im Android-Emulator (Chrome 153, echter Camera2-Weg, dumpsys media.camera)
gegengeprüft: Blitz Aus/Auto/An und Licht kommen richtig bei der Kamera an.
Beim Antippen-Fokus waren zwei Fehler:

- Chrome rechnet pointsOfInterest ungedreht auf den quer liegenden Sensor um,
  das Sucherbild steht aber hochkant. Ein Tipp rechts unten landete an
  falscher Stelle. sensorPunkt() dreht die Tippstelle jetzt in Sensorlage
  zurück (hochkant, quer 90° und 270° gemessen).
- focusMode single-shot nimmt Chrome auf Android an, setzt es aber nicht um
  (kein AF-Trigger). Der Hinweis „Fokus fest“ war damit falsch und ist weg;
  Antippen = Dauer-Autofokus auf die Tippstelle.

Knopf „Kamera wechseln“: weißes Symbol statt 🔄-Emoji (blaues Kästchen),
nur sichtbar, wenn das Gerät mindestens zwei Kameras hat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-10-19 23:12:34 +02:00
parent 2bf5852147
commit 7c8a3c4bc0
4 changed files with 74 additions and 46 deletions

View file

@ -21,9 +21,9 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und
- ✅ **Neuer Auftrag** (FAB): Kunde per Suche/Zuletzt-verwendet wählen, dann **Ihr Zeichen** (Pflicht), **Auftragsbeschreibung** (optional), **Geplanter Liefertermin** (Datum + Uhrzeit) und Checkbox **Auftrag direkt freigeben**. Kunden-Defaults (Zahlungsbedingung/Zahlart, Ansprechpartner) werden übernommen. Bei „direkt freigeben" wird der Auftrag **ohne Position** validiert — die Leistungen kommen aus dem Stundenzettel. (Bis Bericht 1.3.1 hängte das Backend hier eine Std-Lohn-Zeile mit Menge 1 an; die verfälschte den Auftragswert, stand als offene Restmenge in der Lieferauflistung und im Kunden-PDF.) - ✅ **Neuer Auftrag** (FAB): Kunde per Suche/Zuletzt-verwendet wählen, dann **Ihr Zeichen** (Pflicht), **Auftragsbeschreibung** (optional), **Geplanter Liefertermin** (Datum + Uhrzeit) und Checkbox **Auftrag direkt freigeben**. Kunden-Defaults (Zahlungsbedingung/Zahlart, Ansprechpartner) werden übernommen. Bei „direkt freigeben" wird der Auftrag **ohne Position** validiert — die Leistungen kommen aus dem Stundenzettel. (Bis Bericht 1.3.1 hängte das Backend hier eine Std-Lohn-Zeile mit Menge 1 an; die verfälschte den Auftragswert, stand als offene Restmenge in der Lieferauflistung und im Kunden-PDF.)
**Foto-Upload** **Foto-Upload**
- ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung, Fehlschuss löschbar - ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung (nur bei zwei Kameras), Fehlschuss löschbar
- ✅ **Volle Kameraauflösung**: ausgelöst wird über `ImageCapture.takePhoto()` (native JPEG der Kamera inkl. EXIF) — ein Standbild aus dem Video-Stream hat nur Stream-Auflösung und ist nur noch die Rückfallebene (iOS/Safari, ältere Browser) - ✅ **Volle Kameraauflösung**: ausgelöst wird über `ImageCapture.takePhoto()` (native JPEG der Kamera inkl. EXIF) — ein Standbild aus dem Video-Stream hat nur Stream-Auflösung und ist nur noch die Rückfallebene (iOS/Safari, ältere Browser)
- ✅ **Blitz und Fokus** (19.10.2026): Knopf oben schaltet reihum Aus → Auto → An → 🔦 Licht (nur was das Gerät meldet, Auswahl bleibt gemerkt; Blitz wirkt über `takePhoto({fillLightMode})`). Antippen stellt an der Stelle scharf und hält den Fokus („🎯 Fokus fest · zurück auf Auto“). Frontkamera/Tablet-Webcam ohne diese Fähigkeiten: kein Knopf, Antippen ohne Wirkung - ✅ **Blitz und Fokus** (19.10.2026): Knopf oben schaltet reihum Aus → Auto → An → 🔦 Licht (nur was das Gerät meldet, Auswahl bleibt gemerkt; Blitz wirkt über `takePhoto({fillLightMode})`). Antippen stellt den Autofokus auf diese Stelle (Tippstelle wird in Sensorlage zurückgedreht — Chrome tut das nicht; ein echtes „Fokus festhalten“ bietet Chrome auf Android nicht an). Frontkamera/Tablet-Webcam ohne diese Fähigkeiten: kein Knopf, Antippen ohne Wirkung. Knopf „Kamera wechseln“ (weißes Symbol) nur bei mindestens zwei Kameras. Im Android-Emulator per `dumpsys media.camera` gegengeprüft (ROADMAP Punkt 9)
- ✅ **Verkleinerte Fotos heißen `.jpg`** und transparente Bilder bekommen beim Verkleinern weißen Grund (vorher behielt ein verkleinertes AVIF/WebP seinen Namen und wurde vom Server abgelehnt) - ✅ **Verkleinerte Fotos heißen `.jpg`** und transparente Bilder bekommen beim Verkleinern weißen Grund (vorher behielt ein verkleinertes AVIF/WebP seinen Namen und wurde vom Server abgelehnt)
- ✅ Alternativ Aufnahme aus der Galerie (Mehrfachauswahl: alle Dateien werden sofort nach der Auswahl eingelesen, erst dann hochgeladen, weil Android die Galerie-Dateien nur kurz lesbar hält, siehe ROADMAP Punkt 7) - ✅ Alternativ Aufnahme aus der Galerie (Mehrfachauswahl: alle Dateien werden sofort nach der Auswahl eingelesen, erst dann hochgeladen, weil Android die Galerie-Dateien nur kurz lesbar hält, siehe ROADMAP Punkt 7)
- ✅ **Fotoqualität im Admin einstellbar** (Bericht-Modul → Fotoqualität, `api/config.php`): Vorgabe ist **1:1, gar nicht verkleinern**; optional längste Seite + JPEG-Qualität. Gesichert wird immer zuerst das Original, die kleine Fassung ersetzt es danach — hängt das Verkleinern (Display aus), bleibt das Original - ✅ **Fotoqualität im Admin einstellbar** (Bericht-Modul → Fotoqualität, `api/config.php`): Vorgabe ist **1:1, gar nicht verkleinern**; optional längste Seite + JPEG-Qualität. Gesichert wird immer zuerst das Original, die kleine Fassung ersetzt es danach — hängt das Verkleinern (Display aus), bleibt das Original

View file

@ -62,20 +62,37 @@ Eddy: *„beim Fotografieren kann man den Blitz nicht einstellen und den Fokus a
klappt das nicht, wird ohne Einstellung ausgelöst) → 🔦 Licht (Dauerlicht, `track.getCapabilities().torch`, klappt das nicht, wird ohne Einstellung ausgelöst) → 🔦 Licht (Dauerlicht, `track.getCapabilities().torch`,
nach dem Auslösen erneut gesetzt). Meldet das Gerät nichts davon, gibt es keinen Knopf. Auswahl bleibt gemerkt nach dem Auslösen erneut gesetzt). Meldet das Gerät nichts davon, gibt es keinen Knopf. Auswahl bleibt gemerkt
(`localStorage baustelle.kamera.blitz`). (`localStorage baustelle.kamera.blitz`).
- [x] ✅ 19.10.2026: Antippen stellt dort scharf (`pointsOfInterest` + `focusMode: single-shot`, sonst `continuous`; - [x] ✅ 19.10.2026: Antippen stellt dort scharf (`pointsOfInterest` + `focusMode`), Ring an der Stelle, einmaliger
ohne Punktwahl nur neu scharfstellen), Ring an der Stelle. Bei `single-shot` bleibt der Fokus fest, oben steht Hinweis „Antippen stellt dort scharf".
„🎯 Fokus fest · zurück auf Auto" (Tippen → `continuous`). Einmaliger Hinweis „Antippen stellt dort scharf".
- [x] Lokal geprüft mit nachgebildeter Kamera (Canvas-Strom + gemeldete Fähigkeiten): Reihenfolge der Modi, - [x] Lokal geprüft mit nachgebildeter Kamera (Canvas-Strom + gemeldete Fähigkeiten): Reihenfolge der Modi,
Licht an/aus, `takePhoto({fillLightMode:"flash"})`, Antippstelle richtig umgerechnet (eingepasste Vorschau, Licht an/aus, `takePhoto({fillLightMode:"flash"})`, Antippstelle richtig umgerechnet (eingepasste Vorschau,
0,803 statt 0,75 bei seitlichem Rand), „Fokus fest" → continuous; ohne Fähigkeiten kein Knopf, keine Wirkung. 0,803 statt 0,75 bei seitlichem Rand); ohne Fähigkeiten kein Knopf, keine Wirkung.
- [ ] **Am Handy gegenprüfen** (am Schreibtisch nicht nachstellbar): blitzt „An" wirklich, geht „Licht" an, - [x] ✅ 19.10.2026 abends: **Gegenprobe im Android-Emulator** (AVD `fluke`, Chrome 153, emulierte Rückkamera mit
wird nach Antippen die angetippte Stelle scharf. Blitz + AF-Bereich) — echter Camera2-Weg, gemessen per `adb shell dumpsys media.camera` („Last request sent"):
Versuch 19.10.2026 am Rechner Buero: Android-Emulator startet nicht (`SVM disabled (by BIOS)`, außerdem fehlen
nach dem Kernel-Update die Module des laufenden Kernels 7.2.9 → erst nach Neustart + SVM im BIOS). Test mit | Einstellung | was bei der Kamera ankommt | Ergebnis |
der Webcam Razer Kiyo Pro in Chrome: Die Kamera hing schon beim Starten des Bildstroms |---|---|---|
(`uvcvideo: Failed to set UVC commit control: -110`) und fiel danach vom USB-Bus (`error -71`) — also vor | ⚡ Aus | `aeMode ON`, `flash.mode OFF` | ✓ |
Blitz/Fokus. Ergebnis daraus nur: Chrome wirft bei nicht unterstützten Fokus-Vorgaben `OverconstrainedError`, | ⚡ Auto, Auslösen | Standbild `aeMode ON_AUTO_FLASH` | ✓ Kamera entscheidet |
den die App abfängt. | ⚡ An, Auslösen | Standbild `aeMode ON_ALWAYS_FLASH`, `flash.mode SINGLE` | ✓ wie Kamera-Apps |
| 🔦 Licht | `flash.mode TORCH`, bleibt nach dem Auslösen an | ✓ |
| Antippen (alt) | `afRegions` an falscher Stelle, `afMode` bleibt `CONTINUOUS_PICTURE` | ✗ zwei Fehler |
**Fehler 1 — Tippstelle ungedreht:** Chrome rechnet `pointsOfInterest` direkt auf die Sensorfläche um
(`VideoCaptureCamera2.java`, keine Drehung), das Sucherbild kommt aber gedreht an (Sensor quer, Bild hochkant).
Tipp bei 67 % / 82 % → Bereich bei 67 % / 82 % des quer liegenden Sensors statt 82 % / 33 %. Jetzt dreht
`sensorPunkt()` zurück (Rückkamera: Sensor 90° − Bildschirmwinkel; Nothing A059 hat 90°, siehe takePhoto-
Drehmarke 6 in KB #1494). Gemessen: hochkant ✓ (zwei Stellen), quer 90° ✓ (1:1), quer 270° ✓ (180°).
**Fehler 2 — „Fokus fest" log:** `single-shot` nimmt Chrome ohne Fehler an (auch mit `exact`), setzt auf Android
aber nichts um — `AF_TRIGGER_START` kommt im Quelltext nicht vor, der Modus bleibt `CONTINUOUS_PICTURE`, und die
gefundene Entfernung meldet Chrome nicht zurück (nur die eigene Vorgabe). Ein echtes Festhalten geht über die
Web-Schnittstelle also nicht. Jetzt: Antippen = Dauer-Autofokus **auf die Tippstelle**, der Hinweis
„🎯 Fokus fest" ist entfernt.
- [x] ✅ 19.10.2026 (Eddy: „den hässlichen Wechselbutton lässt du da doch nicht"): 🔄-Emoji (erschien als blaues
Kästchen) → weißes Material-Symbol wie ✕; Knopf nur, wenn `enumerateDevices()` mindestens zwei Kameras meldet.
- [ ] **Am Handy beim nächsten Einsatz ansehen** — der Weg bis zur Kamera ist belegt, offen ist nur das Gerät selbst:
blitzt das Nothing bei „An" sichtbar, und springt der Fokus nach dem Antippen sofort auf die Stelle (ohne
AF-Auslöser hängt das vom Kamera-Treiber ab). Wenn nicht: Tag + Uhrzeit reichen.
## 7. Galerie-Auswahl: „permission"-Fehler ab dem 3. Foto (gemeldet 2026-09-30) ## 7. Galerie-Auswahl: „permission"-Fehler ab dem 3. Foto (gemeldet 2026-09-30)

16
app.css
View file

@ -1312,6 +1312,9 @@ body {
.cam-shutter.flash { background: #ffd34d; } .cam-shutter.flash { background: #ffd34d; }
/* Blitz-Knopf: Text statt nur Symbol, damit der Zustand lesbar ist (Aus/Auto/An/Licht) */ /* Blitz-Knopf: Text statt nur Symbol, damit der Zustand lesbar ist (Aus/Auto/An/Licht) */
.camera-modal .cam-blitz { font-size: 15px; white-space: nowrap; } .camera-modal .cam-blitz { font-size: 15px; white-space: nowrap; }
/* Kamera wechseln: weisses Symbol wie ✕, kein Emoji (das erschien als blaues Kaestchen) */
.camera-modal .cam-flip { display: inline-flex; align-items: center; }
.camera-modal .cam-flip svg { display: block; }
/* Scharfstellen per Antippen */ /* Scharfstellen per Antippen */
.camera-modal .cam-fokus-ring { .camera-modal .cam-fokus-ring {
position: absolute; position: absolute;
@ -1326,19 +1329,6 @@ 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; } }
.camera-modal .cam-fokus-fest {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.6);
color: #ffd34d;
border: 1px solid #ffd34d;
border-radius: 16px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
}
.camera-modal [hidden] { display: none !important; } .camera-modal [hidden] { display: none !important; }
/* ============================================================ /* ============================================================

59
app.js
View file

@ -3618,13 +3618,14 @@ async function openCameraModal(orderId) {
<button class="icon-btn" id="cam-close">✕</button> <button class="icon-btn" id="cam-close">✕</button>
<div class="fs-title">📷 Kamera</div> <div class="fs-title">📷 Kamera</div>
<button class="icon-btn cam-blitz" id="cam-blitz" title="Blitz" hidden></button> <button class="icon-btn cam-blitz" id="cam-blitz" title="Blitz" hidden></button>
<button class="icon-btn" id="cam-flip" title="Kamera wechseln">🔄</button> <button class="icon-btn cam-flip" id="cam-flip" title="Kamera wechseln" aria-label="Kamera wechseln" hidden>
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M16 7h-1l-1-1h-4L9 7H8c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm-4 7c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/><path d="M8.57.51l4.48 4.48V2.04c4.72.47 8.48 4.23 8.95 8.95h2C23.34 3.02 15.49-1.59 8.57.51zm2.38 21.45c-4.72-.47-8.48-4.23-8.95-8.95H0c.66 7.97 8.51 12.58 15.43 10.48l-4.48-4.48v2.95z"/></svg>
</button>
</div> </div>
<div class="cam-stage" id="cam-stage"> <div class="cam-stage" id="cam-stage">
<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-fokus-fest" id="cam-fokus-fest" hidden>🎯 Fokus fest · zurück auf Auto</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">
@ -3643,7 +3644,7 @@ async function openCameraModal(orderId) {
const stage = modal.querySelector('#cam-stage'); const stage = modal.querySelector('#cam-stage');
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 fokusFest = modal.querySelector('#cam-fokus-fest'); const flipBtn = modal.querySelector('#cam-flip');
let stream = null; let stream = null;
let facing = 'environment'; let facing = 'environment';
@ -3687,7 +3688,7 @@ async function openCameraModal(orderId) {
modal.querySelector('#cam-close').onclick = () => closeModal(modal); modal.querySelector('#cam-close').onclick = () => closeModal(modal);
modal.querySelector('#cam-done').onclick = () => closeModal(modal); modal.querySelector('#cam-done').onclick = () => closeModal(modal);
modal.querySelector('#cam-flip').onclick = () => { flipBtn.onclick = () => {
facing = (facing === 'environment') ? 'user' : 'environment'; facing = (facing === 'environment') ? 'user' : 'environment';
startStream(); startStream();
}; };
@ -3716,6 +3717,11 @@ async function openCameraModal(orderId) {
video.srcObject = stream; video.srcObject = stream;
await video.play().catch(() => {}); await video.play().catch(() => {});
hint.style.display = 'none'; hint.style.display = 'none';
// Wechselknopf nur, wenn es eine zweite Kamera gibt (Emulator, Tablet-Webcam: nur eine).
// Erst nach der Freigabe ist die Geraeteliste vollstaendig.
navigator.mediaDevices.enumerateDevices().then(l => {
if (my === gen) flipBtn.hidden = l.filter(d => d.kind === 'videoinput').length < 2;
}).catch(() => {});
await kameraExtras(my); await kameraExtras(my);
} catch (err) { } catch (err) {
if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant
@ -3781,8 +3787,10 @@ async function openCameraModal(orderId) {
* - Blitz beim Ausloesen: ImageCapture.getPhotoCapabilities().fillLightMode (auto/flash), * - Blitz beim Ausloesen: ImageCapture.getPhotoCapabilities().fillLightMode (auto/flash),
* wirkt nur ueber takePhoto({ fillLightMode }) — nicht beim Standbild aus dem Sucher. * wirkt nur ueber takePhoto({ fillLightMode }) — nicht beim Standbild aus dem Sucher.
* - Licht (Dauerlicht): track.getCapabilities().torch, per applyConstraints. * - Licht (Dauerlicht): track.getCapabilities().torch, per applyConstraints.
* - Scharfstellen per Antippen: focusMode 'single-shot' (stellt einmal scharf und haelt den * - Scharfstellen per Antippen: pointsOfInterest = angetippte Stelle, focusMode 'continuous'
* Fokus fest) bzw. 'continuous', jeweils mit pointsOfInterest = angetippte Stelle (0..1). * → 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).
* 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' };
@ -3791,7 +3799,7 @@ async function openCameraModal(orderId) {
let blitzModi = []; let blitzModi = [];
let blitz = 'off'; let blitz = 'off';
let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)? let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)?
let fokusModus = ''; // 'single-shot' | 'continuous' | '' (kein Antippen) let fokusModus = ''; // 'continuous' | 'single-shot' | '' (kein Antippen)
let tippHinweis = false; let tippHinweis = false;
function videoTrack() { function videoTrack() {
@ -3800,7 +3808,7 @@ async function openCameraModal(orderId) {
async function kameraExtras(my) { async function kameraExtras(my) {
imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = '';
blitzBtn.hidden = true; fokusFest.hidden = true; fokusRing.hidden = true; blitzBtn.hidden = true; fokusRing.hidden = true;
const track = videoTrack(); const track = videoTrack();
if (!track) return; if (!track) return;
let caps = {}; let caps = {};
@ -3832,7 +3840,7 @@ async function openCameraModal(orderId) {
} }
const fm = Array.isArray(caps.focusMode) ? caps.focusMode : []; const fm = Array.isArray(caps.focusMode) ? caps.focusMode : [];
fokusModus = fm.includes('single-shot') ? 'single-shot' : (fm.includes('continuous') ? 'continuous' : ''); fokusModus = fm.includes('continuous') ? 'continuous' : (fm.includes('single-shot') ? 'single-shot' : '');
if (fokusModus && !tippHinweis) { if (fokusModus && !tippHinweis) {
tippHinweis = true; tippHinweis = true;
hint.textContent = 'Antippen stellt dort scharf'; hint.textContent = 'Antippen stellt dort scharf';
@ -3862,9 +3870,29 @@ async function openCameraModal(orderId) {
try { localStorage.setItem(BLITZ_KEY, blitz); } catch (_) {} try { localStorage.setItem(BLITZ_KEY, blitz); } catch (_) {}
}; };
/* Chrome gibt pointsOfInterest UNGEDREHT an den Sensor weiter (Punkt × Sensorflaeche,
* VideoCaptureCamera2.java) — das Sucherbild kommt aber schon gedreht an: Sensor quer,
* Bild am Handy hochkant. Ohne Rueckdrehung stellte ein Tipp bei 67 % / 82 % auf 67 % / 82 %
* des QUER liegenden Sensors scharf (Emulator, dumpsys afRegions) — also an falscher Stelle.
* Drehung wie in Chrome: Rueckkamera Sensor − Bildschirmwinkel, Frontkamera Sensor + Winkel.
* Sensorlage Rueckkamera 90° (Nothing A059: takePhoto-Drehmarke 6, KB #1494), Frontkamera 270°;
* passt das nicht zum Bildformat (Tablet mit 0°-Sensor), ohne 90°-Anteil. */
function sensorPunkt(x, y) {
const winkel = (screen.orientation && typeof screen.orientation.angle === 'number') ? screen.orientation.angle : 0;
const drehung = (sensor) => (facing === 'user' ? sensor + winkel : sensor - winkel + 360) % 360;
let d = drehung(facing === 'user' ? 270 : 90);
if ((d % 180 === 90) !== (video.videoHeight > video.videoWidth)) d = drehung(0);
switch (d) {
case 90: return { x: y, y: 1 - x };
case 180: return { x: 1 - x, y: 1 - y };
case 270: return { x: 1 - y, y: x };
default: return { x, y };
}
}
stage.addEventListener('click', async (ev) => { stage.addEventListener('click', async (ev) => {
const track = videoTrack(); const track = videoTrack();
if (!track || !fokusModus || !video.videoWidth || ev.target === fokusFest) return; if (!track || !fokusModus || !video.videoWidth) return;
// 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);
@ -3872,6 +3900,7 @@ async function openCameraModal(orderId) {
const x = (ev.clientX - r.left - (r.width - w) / 2) / w; const x = (ev.clientX - r.left - (r.width - w) / 2) / w;
const y = (ev.clientY - r.top - (r.height - h) / 2) / h; const y = (ev.clientY - r.top - (r.height - h) / 2) / h;
if (x < 0 || x > 1 || y < 0 || y > 1) return; if (x < 0 || x > 1 || y < 0 || y > 1) return;
const punkt = sensorPunkt(x, y);
const sr = stage.getBoundingClientRect(); const sr = stage.getBoundingClientRect();
fokusRing.style.left = (ev.clientX - sr.left) + 'px'; fokusRing.style.left = (ev.clientX - sr.left) + 'px';
@ -3883,7 +3912,7 @@ async function openCameraModal(orderId) {
let ok = false; let ok = false;
try { try {
await track.applyConstraints({ advanced: [{ pointsOfInterest: [{ x, y }], focusMode: fokusModus }] }); await track.applyConstraints({ advanced: [{ pointsOfInterest: [punkt], focusMode: fokusModus }] });
ok = true; ok = true;
} catch (_) { } catch (_) {
// Ohne Punktwahl wenigstens neu scharfstellen // Ohne Punktwahl wenigstens neu scharfstellen
@ -3891,16 +3920,8 @@ async function openCameraModal(orderId) {
} }
fokusRing.classList.toggle('ok', ok); fokusRing.classList.toggle('ok', ok);
setTimeout(() => { fokusRing.hidden = true; }, 900); setTimeout(() => { fokusRing.hidden = true; }, 900);
if (ok && fokusModus === 'single-shot') fokusFest.hidden = false;
}); });
fokusFest.onclick = async (ev) => {
ev.stopPropagation();
const track = videoTrack();
fokusFest.hidden = true;
if (track) { try { await track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }); } catch (_) {} }
};
/* Echtes Kamerafoto in voller Sensoraufloesung. /* Echtes Kamerafoto in voller Sensoraufloesung.
* *
* Ein Standbild aus dem Video-Stream hat hoechstens Stream-Aufloesung (hier bis 4K * Ein Standbild aus dem Video-Stream hat hoechstens Stream-Aufloesung (hier bis 4K