Add service worker for push notifications, create calendar layout, and implement WLAN QR code page
- Implemented a service worker (sw.js) to handle push notifications with dynamic options and notification click events. - Created a calendar layout in test.html with a grid system for displaying events across days and times. - Developed a visually engaging WLAN QR code page (wlan.html) with animated backgrounds, particle effects, and tips for connecting to the network.
This commit is contained in:
264
public/led/index.html
Normal file
264
public/led/index.html
Normal file
@@ -0,0 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bild skalieren & anzeigen</title>
|
||||
|
||||
<style>
|
||||
/* Canvas sichtbar skalieren */
|
||||
#outputCanvas {
|
||||
width: 33%;
|
||||
/* Bildschirmbreite */
|
||||
height: auto;
|
||||
/* Höhe automatisch */
|
||||
border: 1px solid black;
|
||||
image-rendering: pixelated;
|
||||
/* WICHTIG: Pixel bleiben scharf */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Bild auswählen, skalieren und im Canvas anzeigen</h2>
|
||||
|
||||
<label>Breite: </label>
|
||||
<input type="number" id="newWidth" value="8"><br>
|
||||
|
||||
<label>Höhe: </label>
|
||||
<input type="number" id="newHeight" value="8"><br><br>
|
||||
<button onclick="myFunction()">Copy text</button>
|
||||
<script>
|
||||
function myFunction() {
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(AnimationData.getPy());
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="file" id="imageInput" accept="image/*,video/*"><br><br>
|
||||
|
||||
<!-- Canvas zur Anzeige -->
|
||||
<canvas id="outputCanvas"></canvas>
|
||||
|
||||
<script>
|
||||
// Pixel-Klasse
|
||||
class Pixel {
|
||||
constructor(x, y, r, g, b, a) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
class Frame {
|
||||
constructor(file) {
|
||||
this.file = URL.createObjectURL(file);
|
||||
this.pixels = [];
|
||||
this.updateSize(8, 8);
|
||||
}
|
||||
|
||||
updateSize(w, h) {
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Interne Pixelgröße des Canvas (NICHT sichtbare Größe)
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
// Bild intern auf dieser Pixelgröße zeichnen
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
// Pixel auslesen
|
||||
const imageData = ctx.getImageData(0, 0, w, h);
|
||||
const data = imageData.data;
|
||||
|
||||
this.pixels = [];
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4;
|
||||
this.pixels.push(new Pixel(x, y, data[i], data[i + 1], data[i + 2], data[i + 3]));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
img.src = this.file;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Animation {
|
||||
constructor(frames, fps = 1) {
|
||||
this.frames = frames;
|
||||
this.fps = fps;
|
||||
this.currentFrameIndex = 0;
|
||||
}
|
||||
|
||||
updateSize(w, h) {
|
||||
this.frames.forEach(f => f.updateSize(w, h));
|
||||
}
|
||||
|
||||
getPy() {
|
||||
|
||||
let string = "";
|
||||
this.frames.forEach((frame, index) => {
|
||||
string += `frame${index} = [`;
|
||||
frame.pixels.forEach((pixel, i) => {
|
||||
const prevPixel = index > 0 ? this.frames[index - 1].pixels[i] : null;
|
||||
if (!prevPixel || prevPixel.r !== pixel.r || prevPixel.g !== pixel.g || prevPixel.b !== pixel.b || prevPixel.a !== pixel.a) {
|
||||
string += `(${i}, ${pixel.r}, ${pixel.g}, ${pixel.b}, ${pixel.a}),`;
|
||||
}
|
||||
});
|
||||
|
||||
string += "]\n";
|
||||
});
|
||||
|
||||
string += "\nFRAMES = [\n";
|
||||
this.frames.forEach((frame, index) => {
|
||||
string += ` frame${index},\n`;
|
||||
});
|
||||
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
getFrame() {
|
||||
return this.frames[this.currentFrameIndex];
|
||||
}
|
||||
|
||||
nextFrame() {
|
||||
this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let AnimationData = new Animation([]);
|
||||
|
||||
|
||||
document.getElementById("imageInput").addEventListener("change", function (event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.type.startsWith("image/")) {
|
||||
AnimationData = new Animation([new Frame(file)], 10);
|
||||
AnimationData.frames[0].updateSize(
|
||||
parseInt(document.getElementById("newWidth").value),
|
||||
parseInt(document.getElementById("newHeight").value)
|
||||
);
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
if (file.type.startsWith("video/")) {
|
||||
|
||||
let targetFPS = 24;
|
||||
let frames = videoDecoder(file, targetFPS);
|
||||
|
||||
AnimationData = new Animation(frames, targetFPS);
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(loop);
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
let accumulator = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
function videoDecoder(file, fps) {
|
||||
let video = document.createElement("video");
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let frames = [];
|
||||
|
||||
function seek(videoEl, time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function cleanup() {
|
||||
videoEl.removeEventListener('seeked', onSeeked);
|
||||
videoEl.removeEventListener('error', onError);
|
||||
}
|
||||
function onSeeked() { cleanup(); resolve(); }
|
||||
function onError(e) { cleanup(); reject(e); }
|
||||
videoEl.addEventListener('seeked', onSeeked);
|
||||
videoEl.addEventListener('error', onError);
|
||||
videoEl.currentTime = Math.min(Math.max(time, 0), videoEl.duration || time);
|
||||
});
|
||||
}
|
||||
|
||||
video.addEventListener('loadeddata', async () => {
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 360;
|
||||
|
||||
let delta = 1 / fps;
|
||||
let currentTime = 0;
|
||||
|
||||
for (let currentTime = 0; currentTime < video.duration; currentTime += delta) {
|
||||
await seek(video, currentTime);
|
||||
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
const url = URL.createObjectURL(blob);
|
||||
let frame = new Frame(blob);
|
||||
document.body.appendChild(document.createTextNode(`Frame at ${currentTime.toFixed(2)}s`));
|
||||
let img = document.createElement("img");
|
||||
img.src = url;
|
||||
//document.body.appendChild(img);
|
||||
document.body.appendChild(document.createElement("br"));
|
||||
frames.push(frame);
|
||||
}, 'image/png');
|
||||
}
|
||||
});
|
||||
|
||||
video.src = URL.createObjectURL(file);
|
||||
video.load();
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loop(time) {
|
||||
const deltaTime = (time - lastTime) / 1000; // in seconds
|
||||
lastTime = time;
|
||||
accumulator += deltaTime;
|
||||
|
||||
const FRAME_TIME = 1 / AnimationData.fps; // ~0.0167 seconds for 60 FPS
|
||||
// Generate frames only when enough time has passed
|
||||
if (accumulator >= FRAME_TIME) {
|
||||
|
||||
const canvas = document.getElementById("outputCanvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const frame = AnimationData.getFrame();
|
||||
AnimationData.nextFrame();
|
||||
|
||||
|
||||
canvas.width = frame.pixels.reduce((max, p) => Math.max(max, p.x), 0) + 1;
|
||||
canvas.height = frame.pixels.reduce((max, p) => Math.max(max, p.y), 0) + 1;
|
||||
|
||||
for (const pixel of frame.pixels) {
|
||||
ctx.fillStyle = `rgba(${pixel.r}, ${pixel.g}, ${pixel.b}, ${pixel.a / 255})`;
|
||||
ctx.fillRect(pixel.x, pixel.y, 1, 1);
|
||||
}
|
||||
|
||||
accumulator -= FRAME_TIME;
|
||||
}
|
||||
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
199
public/led/v.html
Normal file
199
public/led/v.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Video Frame-Stepping mit FPS-Erkennung</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
||||
padding: 18px;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid #ddd;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 10px;
|
||||
color: #444;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Video: Frame für Frame mit FPS-Erkennung</h1>
|
||||
|
||||
<div class="controls">
|
||||
<label>Wähle Datei:
|
||||
<input id="file" type="file" accept="video/*">
|
||||
</label>
|
||||
|
||||
<label>fps:
|
||||
<input id="fps" type="number" step="0.01" value="25" min="1">
|
||||
</label>
|
||||
|
||||
<button id="prev">◀️ Prev Frame</button>
|
||||
<button id="next">Next Frame ▶️</button>
|
||||
<button id="play">Play ▶</button>
|
||||
<button id="pause">Pause ⏸</button>
|
||||
<button id="export">Export Current Frame (PNG)</button>
|
||||
</div>
|
||||
|
||||
<video id="video" controls crossorigin="anonymous" style="display:none"></video>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div class="info" id="info">
|
||||
Lade ein Video, warte auf Metadaten...
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('file');
|
||||
const video = document.getElementById('video');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const fpsInput = document.getElementById('fps');
|
||||
const prevBtn = document.getElementById('prev');
|
||||
const nextBtn = document.getElementById('next');
|
||||
const playBtn = document.getElementById('play');
|
||||
const pauseBtn = document.getElementById('pause');
|
||||
const exportBtn = document.getElementById('export');
|
||||
const info = document.getElementById('info');
|
||||
|
||||
let fileURL = null;
|
||||
let rafId = null;
|
||||
|
||||
// --- Datei laden ---
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
if (fileURL) URL.revokeObjectURL(fileURL);
|
||||
fileURL = URL.createObjectURL(file);
|
||||
video.src = fileURL;
|
||||
video.style.display = 'block';
|
||||
video.load();
|
||||
info.textContent = 'Video geladen. Warte auf Metadaten...';
|
||||
});
|
||||
|
||||
// --- Metadaten geladen ---
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 360;
|
||||
seekAndDraw(0).catch(() => { });
|
||||
info.textContent = `Dauer: ${formatSeconds(video.duration)} — Bildgröße: ${canvas.width}×${canvas.height}`;
|
||||
detectFPS();
|
||||
});
|
||||
|
||||
// --- Hilfsfunktionen ---
|
||||
function formatSeconds(s) {
|
||||
if (!isFinite(s)) return '–';
|
||||
const mm = Math.floor(s / 60);
|
||||
const ss = (s % 60).toFixed(2).padStart(5, '0');
|
||||
return `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
function drawCurrentFrame() {
|
||||
try {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
} catch (e) {
|
||||
console.warn('drawImage fehlgeschlagen:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function seek(videoEl, time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function cleanup() {
|
||||
videoEl.removeEventListener('seeked', onSeeked);
|
||||
videoEl.removeEventListener('error', onError);
|
||||
}
|
||||
function onSeeked() { cleanup(); resolve(); }
|
||||
function onError(e) { cleanup(); reject(e); }
|
||||
videoEl.addEventListener('seeked', onSeeked);
|
||||
videoEl.addEventListener('error', onError);
|
||||
videoEl.currentTime = Math.min(Math.max(time, 0), videoEl.duration || time);
|
||||
});
|
||||
}
|
||||
|
||||
async function seekAndDraw(time) {
|
||||
await seek(video, time);
|
||||
drawCurrentFrame();
|
||||
}
|
||||
|
||||
|
||||
// --- Buttons ---
|
||||
nextBtn.addEventListener('click', async () => {
|
||||
const fps = parseFloat(fpsInput.value) || 25;
|
||||
const frameDur = 1 / fps;
|
||||
const target = (video.currentTime || 0) + frameDur;
|
||||
await seekAndDraw(Math.min(target + 0.00001, video.duration || target));
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', async () => {
|
||||
const fps = parseFloat(fpsInput.value) || 25;
|
||||
const frameDur = 1 / fps;
|
||||
const target = (video.currentTime || 0) - frameDur;
|
||||
await seekAndDraw(Math.max(target, 0));
|
||||
});
|
||||
|
||||
|
||||
exportBtn.addEventListener('click', () => {
|
||||
drawCurrentFrame();
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
const a = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = 'frame.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
||||
}, 'image/png');
|
||||
});
|
||||
|
||||
// --- Seeked Event ---
|
||||
video.addEventListener('seeked', () => { drawCurrentFrame(); });
|
||||
|
||||
// --- Safety Initial Draw ---
|
||||
window.addEventListener('load', () => {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width || 640, canvas.height || 360);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user