553 lines
16 KiB
HTML
553 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="nl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>CAM-01 // Surveillance Feed</title>
|
||
<style>
|
||
/* ============================================================
|
||
CONFIG (makkelijk aanpasbaar)
|
||
============================================================ */
|
||
:root{
|
||
--rec-color:#8a2f28;
|
||
--hud-color:#c7cac7;
|
||
--hud-dim:#6b6f6b;
|
||
--bg-color:#050505;
|
||
--frame-color:#83887f;
|
||
--panel-bg: rgba(12,12,12,0.72);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
html, body {
|
||
margin:0; padding:0; width:100%; height:100%;
|
||
background:var(--bg-color);
|
||
overflow:hidden;
|
||
font-family: "Courier New", ui-monospace, monospace;
|
||
}
|
||
|
||
#stage {
|
||
position:relative;
|
||
width:100vw; height:100vh;
|
||
background:#000;
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:center;
|
||
}
|
||
|
||
/* ============================================================
|
||
LETTERBOX (bioscoop-achtige balken boven/onder)
|
||
============================================================ */
|
||
.letterbox {
|
||
position:absolute;
|
||
left:0; width:100%;
|
||
background:#000;
|
||
z-index:15;
|
||
pointer-events:none;
|
||
}
|
||
#letterbox-top { top:0; height:3.6vh; }
|
||
#letterbox-bottom { bottom:0; height:3.6vh; }
|
||
|
||
/* ============================================================
|
||
VIDEO / MJPEG STREAM
|
||
============================================================ */
|
||
#video-wrapper {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
overflow:hidden;
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:center;
|
||
background:#000;
|
||
}
|
||
|
||
#camera-stream {
|
||
/* ROTATIE: --rotate wordt via JS gezet (0/90/180/270) */
|
||
width:100%;
|
||
height:100%;
|
||
object-fit:cover;
|
||
transform: rotate(var(--rotate, 0deg));
|
||
filter: grayscale(0.55) contrast(1.12) brightness(0.82) saturate(0.75);
|
||
transition: transform 0.3s ease;
|
||
background:#000;
|
||
}
|
||
|
||
#camera-stream.no-signal { display:none; }
|
||
#no-signal-msg {
|
||
display:none;
|
||
color:var(--hud-dim);
|
||
font-size:1.1em;
|
||
letter-spacing:3px;
|
||
text-align:center;
|
||
}
|
||
#no-signal-msg.active { display:block; }
|
||
|
||
/* ============================================================
|
||
OVERLAY EFFECTEN
|
||
============================================================ */
|
||
#effects-layer {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
pointer-events:none;
|
||
}
|
||
|
||
#scanlines {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
background: repeating-linear-gradient(
|
||
to bottom,
|
||
rgba(255,255,255,0.03) 0px,
|
||
rgba(255,255,255,0.03) 1px,
|
||
rgba(0,0,0,0.045) 2px,
|
||
rgba(0,0,0,0.045) 3px
|
||
);
|
||
mix-blend-mode:overlay;
|
||
pointer-events:none;
|
||
z-index:5;
|
||
}
|
||
|
||
/* Zwaardere, filmischere vignette */
|
||
#vignette {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
background: radial-gradient(ellipse at center, rgba(0,0,0,0) 42%, rgba(0,0,0,0.75) 100%);
|
||
pointer-events:none;
|
||
z-index:6;
|
||
}
|
||
|
||
/* Grijze filmkorrel-tint over het geheel */
|
||
#grade-overlay {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
background: linear-gradient(180deg, rgba(120,125,120,0.05), rgba(0,0,0,0.12));
|
||
mix-blend-mode:multiply;
|
||
pointer-events:none;
|
||
z-index:3;
|
||
}
|
||
|
||
#noise-canvas {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
opacity:0.06;
|
||
pointer-events:none;
|
||
z-index:4;
|
||
}
|
||
|
||
#glitch-flash {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
background:rgba(255,255,255,0.035);
|
||
opacity:0;
|
||
pointer-events:none;
|
||
z-index:7;
|
||
}
|
||
#glitch-flash.flash { opacity:1; transition:none; }
|
||
#glitch-flash.fade { opacity:0; transition:opacity 0.15s linear; }
|
||
|
||
/* ============================================================
|
||
HUD
|
||
============================================================ */
|
||
#hud {
|
||
position:absolute;
|
||
top:0; left:0; width:100%; height:100%;
|
||
z-index:10;
|
||
pointer-events:none;
|
||
color:var(--hud-color);
|
||
text-shadow: 0 0 3px rgba(0,0,0,0.95);
|
||
}
|
||
|
||
/* Hoekmarkeringen — puur decoratief, ruim buiten de tekstzones gehouden */
|
||
.corner {
|
||
position:absolute;
|
||
width:26px; height:26px;
|
||
opacity:0.55;
|
||
}
|
||
.corner span { position:absolute; background:var(--frame-color); }
|
||
.corner .h { width:100%; height:1px; }
|
||
.corner .v { width:1px; height:100%; }
|
||
|
||
.corner.tl { top:14px; left:14px; }
|
||
.corner.tl .h { top:0; left:0; }
|
||
.corner.tl .v { top:0; left:0; }
|
||
|
||
.corner.tr { top:14px; right:14px; }
|
||
.corner.tr .h { top:0; right:0; }
|
||
.corner.tr .v { top:0; right:0; }
|
||
|
||
.corner.bl { bottom:14px; left:14px; }
|
||
.corner.bl .h { bottom:0; left:0; }
|
||
.corner.bl .v { bottom:0; left:0; }
|
||
|
||
.corner.br { bottom:14px; right:14px; }
|
||
.corner.br .h { bottom:0; right:0; }
|
||
.corner.br .v { bottom:0; right:0; }
|
||
|
||
/* Center crosshair */
|
||
#crosshair {
|
||
position:absolute;
|
||
top:50%; left:50%;
|
||
width:20px; height:20px;
|
||
transform:translate(-50%,-50%);
|
||
opacity:0.45;
|
||
}
|
||
#crosshair::before, #crosshair::after {
|
||
content:"";
|
||
position:absolute;
|
||
background:var(--frame-color);
|
||
}
|
||
#crosshair::before { top:50%; left:0; width:100%; height:1px; transform:translateY(-50%); }
|
||
#crosshair::after { left:50%; top:0; height:100%; width:1px; transform:translateX(-50%); }
|
||
#crosshair .dot {
|
||
position:absolute; top:50%; left:50%;
|
||
width:2px; height:2px;
|
||
background:var(--frame-color);
|
||
transform:translate(-50%,-50%);
|
||
border-radius:50%;
|
||
}
|
||
|
||
/* ============================================================
|
||
HEADER — alle topinfo in één rustige rij, ruim uit de hoeken
|
||
============================================================ */
|
||
#top-bar {
|
||
position:absolute;
|
||
top:0; left:0; width:100%;
|
||
padding:22px 56px;
|
||
display:flex;
|
||
align-items:flex-start;
|
||
justify-content:space-between;
|
||
font-size:12px;
|
||
letter-spacing:1px;
|
||
background:linear-gradient(to bottom, rgba(0,0,0,0.6), rgba(0,0,0,0));
|
||
}
|
||
|
||
#rec-block {
|
||
display:flex;
|
||
flex-direction:column;
|
||
gap:4px;
|
||
}
|
||
#rec-indicator { display:flex; align-items:center; gap:7px; }
|
||
#rec-dot {
|
||
width:8px; height:8px;
|
||
border-radius:50%;
|
||
background:var(--rec-color);
|
||
box-shadow:0 0 5px var(--rec-color);
|
||
animation: blink 1s steps(1) infinite;
|
||
}
|
||
@keyframes blink {
|
||
0%, 49% { opacity:1; }
|
||
50%, 100% { opacity:0.12; }
|
||
}
|
||
#rec-label { color:var(--rec-color); font-weight:bold; letter-spacing:2px; }
|
||
#rec-timer { color:var(--hud-dim); font-size:11px; letter-spacing:2px; }
|
||
|
||
#cam-name {
|
||
font-size:13px;
|
||
letter-spacing:3px;
|
||
text-align:center;
|
||
align-self:center;
|
||
color:var(--hud-color);
|
||
opacity:0.9;
|
||
}
|
||
|
||
#top-status {
|
||
font-size:11px;
|
||
color:var(--hud-dim);
|
||
text-align:right;
|
||
line-height:1.6;
|
||
}
|
||
|
||
/* ============================================================
|
||
BOTTOM BAR
|
||
============================================================ */
|
||
#bottom-bar {
|
||
position:absolute;
|
||
bottom:0; left:0; width:100%;
|
||
padding:18px 56px;
|
||
display:flex;
|
||
align-items:flex-end;
|
||
justify-content:space-between;
|
||
font-size:11px;
|
||
letter-spacing:1px;
|
||
background:linear-gradient(to top, rgba(0,0,0,0.65), rgba(0,0,0,0));
|
||
}
|
||
|
||
.status-block { display:flex; flex-direction:column; gap:3px; line-height:1.4; }
|
||
.status-block .label { color:var(--hud-dim); font-size:9px; letter-spacing:2px; }
|
||
.status-block .value { color:var(--hud-color); }
|
||
.status-ok { color:#7c8f7c; }
|
||
.status-warn { color:#a8875a; }
|
||
|
||
#datetime-block { text-align:right; }
|
||
|
||
/* ============================================================
|
||
CONTROLE PANEEL — eigen rij, los van hoeken en van bottom-bar
|
||
============================================================ */
|
||
#controls {
|
||
position:absolute;
|
||
bottom:78px;
|
||
right:56px;
|
||
z-index:20;
|
||
display:flex;
|
||
gap:1px;
|
||
pointer-events:auto;
|
||
background:var(--panel-bg);
|
||
border:1px solid rgba(131,136,127,0.4);
|
||
}
|
||
#controls .ctrl-label {
|
||
display:flex;
|
||
align-items:center;
|
||
padding:0 8px;
|
||
font-size:9px;
|
||
letter-spacing:2px;
|
||
color:var(--hud-dim);
|
||
border-right:1px solid rgba(131,136,127,0.3);
|
||
}
|
||
#controls button {
|
||
background:transparent;
|
||
border:none;
|
||
border-right:1px solid rgba(131,136,127,0.25);
|
||
color:var(--hud-color);
|
||
font-family:inherit;
|
||
font-size:11px;
|
||
letter-spacing:1px;
|
||
padding:7px 11px;
|
||
cursor:pointer;
|
||
}
|
||
#controls button:last-child { border-right:none; }
|
||
#controls button:hover { background:rgba(255,255,255,0.06); }
|
||
#controls button.active { background:rgba(255,255,255,0.1); color:#fff; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="stage">
|
||
|
||
<div class="letterbox" id="letterbox-top"></div>
|
||
<div class="letterbox" id="letterbox-bottom"></div>
|
||
|
||
<!-- VIDEO -->
|
||
<div id="video-wrapper">
|
||
<img id="camera-stream"
|
||
src="http://192.168.1.50:8080/video"
|
||
alt="Camera stream"
|
||
onerror="handleStreamError()"
|
||
onload="handleStreamOk()">
|
||
<div id="no-signal-msg">// GEEN SIGNAAL //<br>STREAM ONBEREIKBAAR</div>
|
||
</div>
|
||
|
||
<!-- EFFECTEN -->
|
||
<div id="effects-layer">
|
||
<canvas id="noise-canvas"></canvas>
|
||
<div id="grade-overlay"></div>
|
||
<div id="scanlines"></div>
|
||
<div id="vignette"></div>
|
||
<div id="glitch-flash"></div>
|
||
</div>
|
||
|
||
<!-- HUD -->
|
||
<div id="hud">
|
||
<div class="corner tl"><span class="h"></span><span class="v"></span></div>
|
||
<div class="corner tr"><span class="h"></span><span class="v"></span></div>
|
||
<div class="corner bl"><span class="h"></span><span class="v"></span></div>
|
||
<div class="corner br"><span class="h"></span><span class="v"></span></div>
|
||
|
||
<div id="crosshair"><div class="dot"></div></div>
|
||
|
||
<div id="top-bar">
|
||
<div id="rec-block">
|
||
<div id="rec-indicator">
|
||
<div id="rec-dot"></div>
|
||
<span id="rec-label">REC</span>
|
||
</div>
|
||
<span id="rec-timer">00:00:00</span>
|
||
</div>
|
||
|
||
<div id="cam-name">CAM-01 // VOORINGANG</div>
|
||
|
||
<div id="top-status">
|
||
RES 1920×1080<br>
|
||
FPS <span id="fps-value">--</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="bottom-bar">
|
||
<div class="status-block">
|
||
<span class="label">CAMERA STATUS</span>
|
||
<span class="value status-ok" id="camera-status">ONLINE</span>
|
||
</div>
|
||
<div class="status-block">
|
||
<span class="label">NETWERK</span>
|
||
<span class="value status-ok" id="network-status">VERBONDEN</span>
|
||
</div>
|
||
<div class="status-block">
|
||
<span class="label">MODUS</span>
|
||
<span class="value" id="camera-mode">AUTO / KLEUR</span>
|
||
</div>
|
||
<div class="status-block" id="datetime-block">
|
||
<span class="label">DATUM / TIJD</span>
|
||
<span class="value" id="datetime-value">--:--:--</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ROTATIE CONTROLS — eigen paneel, ruim boven de bottom-bar -->
|
||
<div id="controls">
|
||
<span class="ctrl-label">ROTATIE</span>
|
||
<button data-deg="0" onclick="setRotation(0, this)">0°</button>
|
||
<button data-deg="90" onclick="setRotation(90, this)">90°</button>
|
||
<button data-deg="180" onclick="setRotation(180, this)">180°</button>
|
||
<button data-deg="270" onclick="setRotation(270, this)">270°</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
/* ============================================================
|
||
CONFIGURATIE
|
||
============================================================ */
|
||
const STREAM_URL = "http://192.168.1.50:8080/video"; // <-- pas hier de MJPEG-stream URL aan
|
||
const CAMERA_NAME = "CAM-01 // VOORINGANG"; // <-- pas hier de cameranaam aan
|
||
const DEFAULT_ROTATION = 0; // 0, 90, 180 of 270
|
||
|
||
document.getElementById("camera-stream").src = STREAM_URL;
|
||
document.getElementById("cam-name").textContent = CAMERA_NAME;
|
||
|
||
/* ============================================================
|
||
ROTATIE (puur CSS transform, stream blijft ongewijzigd)
|
||
============================================================ */
|
||
function setRotation(deg, btn) {
|
||
document.documentElement.style.setProperty("--rotate", deg + "deg");
|
||
document.querySelectorAll("#controls button").forEach(b => b.classList.remove("active"));
|
||
if (btn) btn.classList.add("active");
|
||
}
|
||
(function initRotation(){
|
||
const btn = document.querySelector(`#controls button[data-deg="${DEFAULT_ROTATION}"]`);
|
||
setRotation(DEFAULT_ROTATION, btn);
|
||
})();
|
||
|
||
/* ============================================================
|
||
STREAM STATUS HANDLING
|
||
============================================================ */
|
||
let streamOk = true;
|
||
|
||
function handleStreamError() {
|
||
streamOk = false;
|
||
document.getElementById("camera-stream").classList.add("no-signal");
|
||
document.getElementById("no-signal-msg").classList.add("active");
|
||
setStatus("camera-status", "OFFLINE", "status-warn");
|
||
setStatus("network-status", "GEEN VERBINDING", "status-warn");
|
||
}
|
||
|
||
function handleStreamOk() {
|
||
streamOk = true;
|
||
document.getElementById("camera-stream").classList.remove("no-signal");
|
||
document.getElementById("no-signal-msg").classList.remove("active");
|
||
setStatus("camera-status", "ONLINE", "status-ok");
|
||
setStatus("network-status", "VERBONDEN", "status-ok");
|
||
}
|
||
|
||
function setStatus(id, text, cls) {
|
||
const el = document.getElementById(id);
|
||
el.textContent = text;
|
||
el.classList.remove("status-ok", "status-warn");
|
||
el.classList.add(cls);
|
||
}
|
||
|
||
/* ============================================================
|
||
KLOK
|
||
============================================================ */
|
||
function updateClock() {
|
||
const now = new Date();
|
||
const pad = n => String(n).padStart(2, "0");
|
||
const dateStr = `${pad(now.getDate())}-${pad(now.getMonth()+1)}-${now.getFullYear()}`;
|
||
const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
||
document.getElementById("datetime-value").textContent = `${dateStr} ${timeStr}`;
|
||
}
|
||
updateClock();
|
||
setInterval(updateClock, 1000);
|
||
|
||
/* ============================================================
|
||
REC-TIMER (loopt vanaf het laden van de pagina)
|
||
============================================================ */
|
||
const recStart = performance.now();
|
||
function updateRecTimer() {
|
||
const elapsed = Math.floor((performance.now() - recStart) / 1000);
|
||
const h = String(Math.floor(elapsed / 3600)).padStart(2, "0");
|
||
const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0");
|
||
const s = String(elapsed % 60).padStart(2, "0");
|
||
document.getElementById("rec-timer").textContent = `${h}:${m}:${s}`;
|
||
}
|
||
setInterval(updateRecTimer, 1000);
|
||
|
||
/* ============================================================
|
||
FPS INDICATOR (indicatief, gebaseerd op MJPEG frame-updates)
|
||
============================================================ */
|
||
let frameCount = 0;
|
||
let lastFpsCheck = performance.now();
|
||
|
||
document.getElementById("camera-stream").addEventListener("load", () => {
|
||
frameCount++;
|
||
});
|
||
|
||
setInterval(() => {
|
||
const now = performance.now();
|
||
const elapsed = (now - lastFpsCheck) / 1000;
|
||
const fps = streamOk ? Math.round(frameCount / elapsed) : 0;
|
||
document.getElementById("fps-value").textContent = streamOk ? fps : "--";
|
||
frameCount = 0;
|
||
lastFpsCheck = now;
|
||
}, 2000);
|
||
|
||
/* ============================================================
|
||
NOISE EFFECT (lichtgewicht, lage update-frequentie i.v.m. Raspberry Pi)
|
||
============================================================ */
|
||
const noiseCanvas = document.getElementById("noise-canvas");
|
||
const noiseCtx = noiseCanvas.getContext("2d");
|
||
|
||
function resizeNoiseCanvas() {
|
||
noiseCanvas.width = 160;
|
||
noiseCanvas.height = 90;
|
||
}
|
||
resizeNoiseCanvas();
|
||
window.addEventListener("resize", resizeNoiseCanvas);
|
||
|
||
function renderNoise() {
|
||
const w = noiseCanvas.width, h = noiseCanvas.height;
|
||
const imgData = noiseCtx.createImageData(w, h);
|
||
const buffer = imgData.data;
|
||
for (let i = 0; i < buffer.length; i += 4) {
|
||
const shade = Math.random() * 255;
|
||
buffer[i] = shade;
|
||
buffer[i+1] = shade;
|
||
buffer[i+2] = shade;
|
||
buffer[i+3] = 40;
|
||
}
|
||
noiseCtx.putImageData(imgData, 0, 0);
|
||
}
|
||
setInterval(renderNoise, 150);
|
||
|
||
/* ============================================================
|
||
OCCASIONELE SUBTIELE GLITCH FLASH
|
||
============================================================ */
|
||
function triggerGlitch() {
|
||
const flash = document.getElementById("glitch-flash");
|
||
flash.classList.remove("fade");
|
||
flash.classList.add("flash");
|
||
setTimeout(() => {
|
||
flash.classList.remove("flash");
|
||
flash.classList.add("fade");
|
||
}, 60);
|
||
}
|
||
|
||
function scheduleGlitch() {
|
||
const delay = 5000 + Math.random() * 9000;
|
||
setTimeout(() => {
|
||
triggerGlitch();
|
||
scheduleGlitch();
|
||
}, delay);
|
||
}
|
||
scheduleGlitch();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|