Files
waste-of-space/index.html
T
2026-07-22 17:39:11 +02:00

553 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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)"></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>