From 361ffc5578112c95a9432329baa77f80e6eb50d0 Mon Sep 17 00:00:00 2001 From: Tot Maxim Date: Mon, 11 May 2026 16:18:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BA=D1=80=D0=B5=D1=81=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BA=D0=B8=20=D0=BD=D0=B0=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=D0=B5=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/web/css/style.css | 64 +++++++++++++++++++++++ cmd/server/web/dashboard.html | 14 +++++- cmd/server/web/js/camera.js | 95 +++++++++++++++++++++++++++++++++-- 3 files changed, 167 insertions(+), 6 deletions(-) diff --git a/cmd/server/web/css/style.css b/cmd/server/web/css/style.css index 73c4b5e..50d7fb0 100644 --- a/cmd/server/web/css/style.css +++ b/cmd/server/web/css/style.css @@ -341,6 +341,70 @@ h1 { object-fit: contain; background: black; } +/* ===== CAMERA OVERLAY ===== */ +.cam-frame-wrapper { + position: relative; + flex: 1; + width: 100%; + background: black; + overflow: hidden; +} + +.cam-frame { + width: 100%; + height: 100%; + object-fit: contain; + background: black; +} + +/* Перекрестие (crosshair) */ +.cam-crosshair { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; +} + +/* Вертикальная линия */ +.crosshair-v { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 1px; + height: 100%; + background: #00ff88; + box-shadow: 0 0 4px rgba(0, 255, 136, 0.3); +} + +/* Горизонтальная линия */ +.crosshair-h { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + width: 100%; + height: 1px; + background: #00ff88; + box-shadow: 0 0 4px rgba(0, 255, 136, 0.3); +} + +/* Центральный квадрат */ +.crosshair-box { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 120px; + height: 120px; + border: 2px solid #00ff88; + box-shadow: + 0 0 10px rgba(0, 255, 136, 0.3), + inset 0 0 10px rgba(0, 255, 136, 0.1); +} /* ===== MOBILE ===== */ @media (max-width: 500px) { diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html index 0dc2555..7687d29 100644 --- a/cmd/server/web/dashboard.html +++ b/cmd/server/web/dashboard.html @@ -86,7 +86,19 @@
Видео с камеры
- Camera +
+ Camera + + +
+ +
+
+ + +
+
+
diff --git a/cmd/server/web/js/camera.js b/cmd/server/web/js/camera.js index 6abd18e..3f33fa0 100644 --- a/cmd/server/web/js/camera.js +++ b/cmd/server/web/js/camera.js @@ -3,14 +3,17 @@ import { updateResizerVisibility } from './layout.js'; let rightPanel; let camImage; -let camClose; - +let crosshair; let isOpen = false; +let streamCheckInterval = null; export function initCamera() { rightPanel = document.getElementById("rightPanel"); camImage = document.getElementById("camImage"); - camClose = document.getElementById("camClose"); + crosshair = document.getElementById("crosshair"); + + // Проверяем доступность потока + checkStreamAvailability(); const btn = document.getElementById("camToggle"); @@ -23,6 +26,11 @@ export function initCamera() { rightPanel.classList.remove("hidden"); + // Показываем перекрестие + if (crosshair) { + crosshair.style.display = "block"; + } + camImage.src = streamUrl; if (btn) { @@ -32,6 +40,9 @@ export function initCamera() { console.log("[camera] opened"); updateResizerVisibility(); + + // Запускаем мониторинг потока + startStreamMonitoring(); } function close() { @@ -39,6 +50,11 @@ export function initCamera() { isOpen = false; + // Скрываем перекрестие + if (crosshair) { + crosshair.style.display = "none"; + } + // остановка stream camImage.src = ""; @@ -51,6 +67,9 @@ export function initCamera() { console.log("[camera] closed"); updateResizerVisibility(); + + // Останавливаем мониторинг + stopStreamMonitoring(); } function toggle() { @@ -63,14 +82,80 @@ export function initCamera() { camImage.onload = () => { console.log("[camera] stream started"); + if (crosshair) { + crosshair.style.opacity = '1'; + } }; camImage.onerror = (e) => { console.error("[camera] stream error", e); + if (crosshair) { + crosshair.style.opacity = '0.3'; + } }; btn?.addEventListener("click", toggle); - // автозапуск - open(); + // Проверка доступности камеры + async function checkStreamAvailability() { + try { + const response = await fetch('/api/stream'); + const data = await response.json(); + + if (data.available && data.cam) { + console.log("[camera] stream available:", data.source); + // Автозапуск если камера доступна + if (!isOpen) { + open(); + } + } else { + console.log("[camera] stream not available"); + if (crosshair) { + crosshair.style.display = 'none'; + } + } + } catch (error) { + console.log("[camera] check failed:", error); + } + } + + // Мониторинг состояния потока + function startStreamMonitoring() { + stopStreamMonitoring(); + streamCheckInterval = setInterval(() => { + if (isOpen && camImage) { + if (!camImage.complete || camImage.naturalWidth === 0) { + if (crosshair) { + crosshair.style.opacity = '0.3'; + } + } else { + if (crosshair) { + crosshair.style.opacity = '1'; + } + } + } + }, 2000); + } + + function stopStreamMonitoring() { + if (streamCheckInterval) { + clearInterval(streamCheckInterval); + streamCheckInterval = null; + } + } +} + +// Экспортируем для возможности ручного управления +export function showCrosshair(show = true) { + const crosshair = document.getElementById("crosshair"); + if (crosshair) { + crosshair.style.display = show ? "block" : "none"; + } +} + +export function setCrosshairOpacity(opacity) { + const crosshair = document.getElementById("crosshair"); + if (crosshair) { + crosshair.style.opacity = opacity; + } } \ No newline at end of file