diff --git a/app.js b/app.js index 870a244..75bfa88 100644 --- a/app.js +++ b/app.js @@ -1320,7 +1320,11 @@ async function resizeImage(file, maxSide, timeoutMs, quality) { const c = document.createElement('canvas'); c.width = Math.round(img.width * scale); c.height = Math.round(img.height * scale); - c.getContext('2d').drawImage(img, 0, 0, c.width, c.height); + const ctx = c.getContext('2d'); + // Weisser Grund: JPEG kennt keine Transparenz, ein freigestelltes PNG/WebP wuerde sonst schwarz + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, c.width, c.height); + ctx.drawImage(img, 0, 0, c.width, c.height); c.toBlob(b => resolve(b || file), 'image/jpeg', typeof quality === 'number' ? quality : 0.85); }; img.onerror = () => { URL.revokeObjectURL(url); resolve(file); }; @@ -3613,11 +3617,14 @@ async function openCameraModal(orderId) {
📷 Kamera
+
-
+
Kamera wird gestartet…
+ +
@@ -3633,6 +3640,10 @@ async function openCameraModal(orderId) { const hint = modal.querySelector('#cam-hint'); const countEl = modal.querySelector('#cam-count'); const shutter = modal.querySelector('#cam-shutter'); + const stage = modal.querySelector('#cam-stage'); + const blitzBtn = modal.querySelector('#cam-blitz'); + const fokusRing = modal.querySelector('#cam-fokus-ring'); + const fokusFest = modal.querySelector('#cam-fokus-fest'); let stream = null; let facing = 'environment'; @@ -3705,6 +3716,7 @@ async function openCameraModal(orderId) { video.srcObject = stream; await video.play().catch(() => {}); hint.style.display = 'none'; + await kameraExtras(my); } catch (err) { if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant console.warn('[Kamera]', err); @@ -3763,6 +3775,132 @@ async function openCameraModal(orderId) { } } + /* Blitz und Fokus (Eddy, 19.10.2026: „den Blitz kann man nicht einstellen und den Fokus auch nicht"). + * + * Angeboten wird nur, was das Geraet meldet — Chrome kennt es erst, wenn der Strom laeuft: + * - Blitz beim Ausloesen: ImageCapture.getPhotoCapabilities().fillLightMode (auto/flash), + * wirkt nur ueber takePhoto({ fillLightMode }) — nicht beim Standbild aus dem Sucher. + * - Licht (Dauerlicht): track.getCapabilities().torch, per applyConstraints. + * - Scharfstellen per Antippen: focusMode 'single-shot' (stellt einmal scharf und haelt den + * Fokus fest) bzw. 'continuous', jeweils mit pointsOfInterest = angetippte Stelle (0..1). + * 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_KEY = 'baustelle.kamera.blitz'; + let imageCap = null; + let blitzModi = []; + let blitz = 'off'; + let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)? + let fokusModus = ''; // 'single-shot' | 'continuous' | '' (kein Antippen) + let tippHinweis = false; + + function videoTrack() { + return stream ? stream.getVideoTracks()[0] || null : null; + } + + async function kameraExtras(my) { + imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = ''; + blitzBtn.hidden = true; fokusFest.hidden = true; fokusRing.hidden = true; + const track = videoTrack(); + if (!track) return; + let caps = {}; + try { caps = typeof track.getCapabilities === 'function' ? (track.getCapabilities() || {}) : {}; } catch (_) {} + let licht = []; + if (typeof ImageCapture === 'function') { + try { + imageCap = new ImageCapture(track); + const pc = await imageCap.getPhotoCapabilities(); + licht = Array.isArray(pc && pc.fillLightMode) ? pc.fillLightMode : []; + } catch (_) { /* kein Standbild-Erfasser — Sucherbild bleibt */ } + } + if (my !== gen) return; // inzwischen geschlossen/gewechselt + + fotoBlitz = licht.includes('auto') || licht.includes('flash'); + const modi = []; + if (fotoBlitz || caps.torch) modi.push('off'); + if (licht.includes('auto')) modi.push('auto'); + if (licht.includes('flash')) modi.push('flash'); + if (caps.torch) modi.push('torch'); + blitzModi = modi; + let gemerkt = 'off'; + try { gemerkt = localStorage.getItem(BLITZ_KEY) || 'off'; } catch (_) {} + blitz = modi.includes(gemerkt) ? gemerkt : 'off'; + if (modi.length > 1) { + blitzBtn.hidden = false; + blitzBtn.textContent = BLITZ_TEXT[blitz]; + if (blitz === 'torch') lichtSchalten(true); + } + + const fm = Array.isArray(caps.focusMode) ? caps.focusMode : []; + fokusModus = fm.includes('single-shot') ? 'single-shot' : (fm.includes('continuous') ? 'continuous' : ''); + if (fokusModus && !tippHinweis) { + tippHinweis = true; + hint.textContent = 'Antippen stellt dort scharf'; + hint.style.display = ''; + setTimeout(() => { if (hint.textContent === 'Antippen stellt dort scharf') hint.style.display = 'none'; }, 2500); + } + } + + async function lichtSchalten(an) { + const track = videoTrack(); + if (!track) return false; + try { await track.applyConstraints({ advanced: [{ torch: an }] }); return true; } + catch (_) { return false; } + } + + blitzBtn.onclick = async () => { + if (blitzModi.length < 2) return; + const vorher = blitz; + blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length]; + if (vorher === 'torch' || blitz === 'torch') { + if (!await lichtSchalten(blitz === 'torch') && blitz === 'torch') { + showToast('Das Licht lässt sich hier nicht schalten', 'warn'); + blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length]; + } + } + blitzBtn.textContent = BLITZ_TEXT[blitz]; + try { localStorage.setItem(BLITZ_KEY, blitz); } catch (_) {} + }; + + stage.addEventListener('click', async (ev) => { + const track = videoTrack(); + if (!track || !fokusModus || !video.videoWidth || ev.target === fokusFest) return; + // 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); + const w = video.videoWidth * sk, h = video.videoHeight * sk; + const x = (ev.clientX - r.left - (r.width - w) / 2) / w; + const y = (ev.clientY - r.top - (r.height - h) / 2) / h; + if (x < 0 || x > 1 || y < 0 || y > 1) return; + + const sr = stage.getBoundingClientRect(); + fokusRing.style.left = (ev.clientX - sr.left) + 'px'; + fokusRing.style.top = (ev.clientY - sr.top) + 'px'; + fokusRing.hidden = false; + fokusRing.classList.remove('ok', 'aktiv'); + void fokusRing.offsetWidth; // Animation neu starten + fokusRing.classList.add('aktiv'); + + let ok = false; + try { + await track.applyConstraints({ advanced: [{ pointsOfInterest: [{ x, y }], focusMode: fokusModus }] }); + ok = true; + } catch (_) { + // Ohne Punktwahl wenigstens neu scharfstellen + try { await track.applyConstraints({ advanced: [{ focusMode: fokusModus }] }); ok = true; } catch (_) {} + } + fokusRing.classList.toggle('ok', ok); + 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. * * Ein Standbild aus dem Video-Stream hat hoechstens Stream-Aufloesung (hier bis 4K @@ -3777,8 +3915,15 @@ async function openCameraModal(orderId) { if (typeof ImageCapture === 'undefined' || !stream) return null; const track = stream.getVideoTracks()[0]; if (!track || typeof track.getCapabilities !== 'function') return null; - const cap = new ImageCapture(track); - const blob = await cap.takePhoto(); + const cap = imageCap || new ImageCapture(track); + let blob = null; + if (fotoBlitz && blitz !== 'torch') { + // Blitz ausdruecklich setzen (auch „aus"); klappt das nicht, ohne Einstellungen ausloesen + try { blob = await cap.takePhoto({ fillLightMode: blitz }); } catch (_) { blob = null; } + } + if (!blob) blob = await cap.takePhoto(); + // Manche Geraete schalten das Dauerlicht beim Ausloesen ab + if (blitz === 'torch') lichtSchalten(true); return (blob && blob.size > 0) ? blob : null; } catch (_) { return null; // z.B. „takePhoto not supported" — Standbild nehmen diff --git a/lib/offline.js b/lib/offline.js index c578113..366a79d 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -74,6 +74,11 @@ const it = await idb.queuePatch(id, (x) => { x.data = buf; x.mime = fileBlob.type || x.mime || 'image/jpeg'; + // Verkleinert = JPEG aus dem Canvas: Endung angleichen. Sonst laege JPEG-Inhalt unter + // „.avif"/„.webp" in der Queue, und der Server lehnt die Endung ab. + if (x.mime === 'image/jpeg' && x.filename && !/\.jpe?g$/i.test(x.filename)) { + x.filename = x.filename.replace(/\.[^.\/]*$/, '') + '.jpg'; + } }); return !!it; // null: schon hochgeladen und geloescht }