add noise canvas for loading indicator and improve video loading experience

This commit is contained in:
zwitschi committed 2026-07-24 15:31:20 +02:00
1 parent b562b57bb4
commit 443ed74093
3 files changed
+89 -5

No files matched your search

+20 -3
View File
@@ -12,8 +12,9 @@
--radius: 12px; --radius: 12px;
--radius-sm: 8px; --radius-sm: 8px;
--shadow: 0 8px 30px rgba(0, 0, 0, 0.5); --shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
--font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, --font:
Helvetica, Arial, sans-serif; "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
} }
* { * {
@@ -95,7 +96,9 @@ header {
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
outline: none; outline: none;
transition: border-color 0.2s, box-shadow 0.2s; transition:
border-color 0.2s,
box-shadow 0.2s;
} }
.search-wrap input:focus { .search-wrap input:focus {
border-color: var(--accent); border-color: var(--accent);
@@ -166,6 +169,20 @@ header {
object-fit: contain; object-fit: contain;
background: #000; background: #000;
} }
.noise-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
opacity: 1;
image-rendering: pixelated;
transition: opacity 0.3s ease;
}
.noise-canvas.hidden {
opacity: 0;
}
.video-placeholder { .video-placeholder {
position: absolute; position: absolute;
inset: 0; inset: 0;
+64 -1
View File
@@ -30,6 +30,7 @@
}); });
const player = document.getElementById("player"); const player = document.getElementById("player");
const noiseCanvas = document.getElementById("noiseCanvas");
const placeholder = document.getElementById("videoPlaceholder"); const placeholder = document.getElementById("videoPlaceholder");
const badge = document.getElementById("videoBadge"); const badge = document.getElementById("videoBadge");
const badgeText = document.getElementById("badgeText"); const badgeText = document.getElementById("badgeText");
@@ -50,6 +51,65 @@
let sortMode = "oldest"; let sortMode = "oldest";
let current = null; let current = null;
// ---- Static noise canvas for loading indicator ----
var noiseCtx = noiseCanvas.getContext("2d");
var noiseAnimId = null;
var noiseRunning = false;
function noiseStart() {
if (noiseRunning) return;
noiseRunning = true;
noiseCanvas.classList.remove("hidden");
noiseAnimId = null;
noiseTick();
}
function noiseStop() {
noiseRunning = false;
noiseCanvas.classList.add("hidden");
if (noiseAnimId) {
cancelAnimationFrame(noiseAnimId);
noiseAnimId = null;
}
}
function noiseTick() {
if (!noiseRunning) return;
var w = noiseCanvas.width;
var h = noiseCanvas.height;
var rect = noiseCanvas.getBoundingClientRect();
// Render at reduced resolution (160px wide) for performance,
// CSS image-rendering: pixelated handles the upscale.
var targetW = Math.max(1, Math.min(160, (rect.width * 0.25) | 0));
var ratio = rect.height / Math.max(1, rect.width);
var targetH = Math.max(1, (targetW * ratio) | 0);
if (w !== targetW || h !== targetH) {
noiseCanvas.width = targetW;
noiseCanvas.height = targetH;
w = targetW;
h = targetH;
}
if (w === 0 || h === 0) {
noiseAnimId = requestAnimationFrame(noiseTick);
return;
}
var imageData = noiseCtx.createImageData(w, h);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
var v = (Math.random() * 255) | 0;
data[i] = v;
data[i + 1] = v;
data[i + 2] = v;
data[i + 3] = 255;
}
noiseCtx.putImageData(imageData, 0, 0);
noiseAnimId = requestAnimationFrame(noiseTick);
}
// Start noise immediately on page load (no video selected yet)
noiseStart();
// ---- Prefetch helpers ---- // ---- Prefetch helpers ----
// Track already-prefetched URLs so we don't re-fetch // Track already-prefetched URLs so we don't re-fetch
@@ -262,9 +322,11 @@
player.src = clip.url; player.src = clip.url;
player.load(); player.load();
// Start playback first, prefetch only after the video is actually playing // Show noise while loading, stop when video starts playing
noiseStart();
var prefetchDone = false; var prefetchDone = false;
function onPlaying() { function onPlaying() {
noiseStop();
if (prefetchDone) return; if (prefetchDone) return;
prefetchDone = true; prefetchDone = true;
player.removeEventListener("playing", onPlaying); player.removeEventListener("playing", onPlaying);
@@ -273,6 +335,7 @@
player.addEventListener("playing", onPlaying); player.addEventListener("playing", onPlaying);
player.play().catch(function () { player.play().catch(function () {
// Even if autoplay is blocked, still prefetch for when the user clicks play // Even if autoplay is blocked, still prefetch for when the user clicks play
noiseStop();
if (!prefetchDone) { if (!prefetchDone) {
prefetchDone = true; prefetchDone = true;
player.removeEventListener("playing", onPlaying); player.removeEventListener("playing", onPlaying);
+5 -1
View File
@@ -4,7 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TeslaCam Video Player</title> <title>TeslaCam Video Player</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
</head> </head>
<body> <body>
<!-- Header --> <!-- Header -->
@@ -56,6 +59,7 @@
<section class="player-panel"> <section class="player-panel">
<div class="video-stage"> <div class="video-stage">
<video id="player" controls preload="metadata" playsinline></video> <video id="player" controls preload="metadata" playsinline></video>
<canvas id="noiseCanvas" class="noise-canvas"></canvas>
<div class="video-badge" id="videoBadge"> <div class="video-badge" id="videoBadge">
<span class="dot"></span><span id="badgeText">Front</span> <span class="dot"></span><span id="badgeText">Front</span>
</div> </div>