Webcam: Zaehler-Box als verschiebbares HTML-Overlay (per Maus)

Statt ins Bild gebrannt ist der Zaehler jetzt ein HTML-Overlay, das frei
mit der Maus positioniert werden kann; Position wird in localStorage
gemerkt. Werte kommen ueber /api/counts (Live-Polling).

- draw_overlay/render_static/process_frame: draw_counter-Flag
  (Webcam laesst Box weg, Video-Upload behaelt sie eingebrannt)
- Grabber-State auf self.state -> /api/counts liest live mit
- Drag-Logik + Counts-Polling im Frontend

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 17:15:54 +02:00
parent 07c0e44cd8
commit 7a522f8927
2 changed files with 110 additions and 15 deletions

View File

@@ -67,6 +67,24 @@
.offline-overlay .icon { font-size: 54px; }
.offline-overlay .msg { font-size: 22px; font-weight: 600; }
.offline-overlay .sub { font-size: 14px; opacity: 0.8; }
.counter-box {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.78);
color: #fff;
padding: 8px 14px 10px;
border-radius: 6px;
font-family: monospace;
font-size: 14px;
line-height: 1.5;
cursor: move;
user-select: none;
z-index: 4;
min-width: 150px;
}
.counter-box .cb-total { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.counter-box .cb-grip { opacity: 0.5; font-size: 11px; margin-top: 4px; }
.video-container {
position: relative;
width: 1020px;
@@ -143,6 +161,14 @@
<div class="video-container">
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
<canvas id="lineCanvas"></canvas>
<div id="counterBox" class="counter-box" title="Zum Verschieben ziehen">
<div class="cb-total">Gesamt: <span id="cTotal">0</span></div>
<div>Autos: <span id="cCar">0</span></div>
<div>LKW: <span id="cTruck">0</span></div>
<div>Busse: <span id="cBus">0</span></div>
<div>Motorräder: <span id="cMoto">0</span></div>
<div class="cb-grip">⠿ ziehen</div>
</div>
<div id="offlineOverlay" class="offline-overlay">
<div class="icon" id="ovIcon">📷</div>
<div class="msg" id="ovMsg">Kamera offline</div>
@@ -369,6 +395,57 @@
checkCamStatus();
setInterval(checkCamStatus, 2000);
// --- Zaehler-Box: Werte pollen ---
const cEl = {
total: document.getElementById('cTotal'),
car: document.getElementById('cCar'),
truck: document.getElementById('cTruck'),
bus: document.getElementById('cBus'),
moto: document.getElementById('cMoto'),
};
async function updateCounts() {
try {
const d = await (await fetch('/api/counts', { cache: 'no-store' })).json();
cEl.total.textContent = d.total;
cEl.car.textContent = d.car;
cEl.truck.textContent = d.truck;
cEl.bus.textContent = d.bus;
cEl.moto.textContent = d.motorcycle;
} catch (e) { /* ignorieren */ }
}
updateCounts();
setInterval(updateCounts, 1000);
// --- Zaehler-Box: per Maus verschiebbar (Position gemerkt) ---
const counterBox = document.getElementById('counterBox');
const videoContainer = document.querySelector('.video-container');
const savedPos = JSON.parse(localStorage.getItem('counterPos') || 'null');
if (savedPos) {
counterBox.style.left = savedPos.x + 'px';
counterBox.style.top = savedPos.y + 'px';
}
let dragging = false, offX = 0, offY = 0;
counterBox.addEventListener('mousedown', (e) => {
dragging = true;
offX = e.clientX - counterBox.offsetLeft;
offY = e.clientY - counterBox.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const maxX = videoContainer.clientWidth - counterBox.offsetWidth;
const maxY = videoContainer.clientHeight - counterBox.offsetHeight;
const x = Math.max(0, Math.min(e.clientX - offX, maxX));
const y = Math.max(0, Math.min(e.clientY - offY, maxY));
counterBox.style.left = x + 'px';
counterBox.style.top = y + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
localStorage.setItem('counterPos', JSON.stringify({ x: counterBox.offsetLeft, y: counterBox.offsetTop }));
});
</script>
<script>
function applyThemeBtn(){var d=document.documentElement.getAttribute('data-theme')==='dark';var b=document.getElementById('themeToggle');if(b)b.textContent=d?'☀️':'🌙';}