P42 Dashboard: erster Stand
FastAPI-Backend und React-Frontend mit Konten, Sendungsverfolgung, Projekt-Artikeln, Vorrat & Einkauf mit Rezeptabgleich, Reise-Weltkarte, Kanban-Projekten mit Priorität, Server-Monitoring und den Designs Matrix und Blackwall. Betrieb per Docker hinter nginx (siehe DEPLOY.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
dd58b3d1f8
75 files changed
+11418
No files matched your search
@@ -0,0 +1,259 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { isCalm, onMotionChange } from '../motion.js';
|
||||
|
||||
// Hintergrund "Blackwall" nach Einzelbildern aus dem Referenzvideo (Cyberpunk 2077):
|
||||
// - Wand-Korridor: senkrechte Lichtlinien, die perspektivisch auf einen Fluchtpunkt am Horizont zulaufen
|
||||
// (am Rand hoch, zur Mitte flach); Daten fließen sichtbar an ihnen herab
|
||||
// - Boden: Feld aus Lichtpunkten in Perspektive, das langsam auf den Betrachter zufließt
|
||||
// - Horizont: gleißendes Band mit wanderndem Hotspot, davor eine zerfaserte Skyline aus Blöcken
|
||||
// - Zeichenregen nur an den Rändern, seltener Glitch (Zeilenversatz, kein Helligkeitsblitz)
|
||||
// Farben: Pink-Magenta mit vereinzelten violett-blauen Linien. Ohne Animation: ein Standbild.
|
||||
const FPS = 24;
|
||||
const GLITCH_EVERY = [4500, 9000];
|
||||
const GLITCH_FOR = 160;
|
||||
const GLYPHS = '0123456789ABCDEF#$%&*+<>/\\|';
|
||||
|
||||
const PINK = [255, 32, 92];
|
||||
const HOT = [255, 110, 160];
|
||||
const VIOLET = [140, 110, 255];
|
||||
|
||||
const hash = (n) => {
|
||||
const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
const rgba = ([r, g, b], a) => `rgba(${r},${g},${b},${Math.max(0, Math.min(1, a))})`;
|
||||
|
||||
export default function BlackwallField() {
|
||||
const ref = useRef(null);
|
||||
const [calm, setCalm] = useState(isCalm);
|
||||
|
||||
useEffect(() => onMotionChange(() => setCalm(isCalm())), []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = ref.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
let dpr = 1;
|
||||
let lines = [];
|
||||
let blocks = [];
|
||||
let columns = [];
|
||||
let frame = 0;
|
||||
let last = 0;
|
||||
let nextGlitch = performance.now() + 3500;
|
||||
let glitchUntil = 0;
|
||||
|
||||
function build() {
|
||||
const cx = w / 2;
|
||||
// Wandlinien: dicht (alle ~3 px), Höhe nach Perspektive
|
||||
lines = Array.from({ length: Math.round(w / 3) }, (_, i) => {
|
||||
const x = hash(i + 1) * w;
|
||||
const persp = Math.min(1, Math.abs(x - cx) / (w / 2)); // 0 Mitte … 1 Rand
|
||||
const tint = hash(i + 7);
|
||||
return {
|
||||
x,
|
||||
width: hash(i + 2) > 0.9 ? 2 : 1,
|
||||
up: h * (0.03 + 0.46 * persp ** 1.25) * (0.55 + 0.45 * hash(i + 3)), // über dem Horizont
|
||||
down: h * (0.01 + 0.16 * persp ** 1.4) * (0.4 + 0.6 * hash(i + 4)), // unter dem Horizont
|
||||
alpha: 0.08 + hash(i + 5) ** 2 * 0.55,
|
||||
flow: 30 + hash(i + 6) * 90, // px/s Datenfluss
|
||||
phase: hash(i + 8) * 10,
|
||||
color: tint > 0.92 ? VIOLET : tint > 0.78 ? HOT : PINK,
|
||||
};
|
||||
});
|
||||
// Zerfaserte Skyline am Horizont, zur Mitte gehäuft
|
||||
blocks = Array.from({ length: Math.round(w / 12) }, (_, i) => {
|
||||
const x = w * (0.5 + (hash(i + 101) - 0.5) * (0.4 + hash(i + 107) * 0.95));
|
||||
return {
|
||||
x,
|
||||
bw: 8 + hash(i + 103) ** 2 * w * 0.08,
|
||||
bh: 6 + hash(i + 104) ** 1.5 * h * 0.12,
|
||||
lift: hash(i + 102) * 0.85, // wie weit der Block über den Horizont ragt
|
||||
alpha: 0.05 + hash(i + 105) * 0.26,
|
||||
drift: (hash(i + 106) - 0.5) * 5,
|
||||
};
|
||||
});
|
||||
// Zeichenspalten nur an den Rändern (nur auf breiten Bildschirmen)
|
||||
columns = [];
|
||||
if (w > 700) {
|
||||
const step = 18;
|
||||
for (let x = 6; x < w; x += step) {
|
||||
if (x > w * 0.14 && x < w * 0.86) continue;
|
||||
const i = columns.length;
|
||||
if (hash(i + 301) < 0.45) continue;
|
||||
columns.push({ x, speed: 40 + hash(i + 302) * 90, offset: hash(i + 303) * h * 2, len: 8 + Math.floor(hash(i + 304) * 14), seed: i });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resize() {
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
build();
|
||||
draw(performance.now());
|
||||
}
|
||||
|
||||
function draw(now) {
|
||||
const t = calm ? 7 : now / 1000;
|
||||
const cy = h * 0.52; // Horizont
|
||||
const cx = w * 0.5;
|
||||
const hotX = cx + Math.sin(t * 0.13) * w * 0.16; // wandernder Lichtfleck
|
||||
const breath = 0.86 + 0.1 * Math.sin(t * 0.7) + 0.04 * Math.sin(t * 1.9 + 1.3);
|
||||
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.fillStyle = '#040002';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
|
||||
// 1) Dunst um den Horizont
|
||||
let g = ctx.createLinearGradient(0, cy - h * 0.4, 0, cy + h * 0.3);
|
||||
g.addColorStop(0, rgba(PINK, 0));
|
||||
g.addColorStop(0.58, rgba(PINK, 0.26 * breath));
|
||||
g.addColorStop(1, rgba(PINK, 0));
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, cy - h * 0.4, w, h * 0.7);
|
||||
|
||||
// 2) Boden: Lichtpunkte in Perspektive, fließen langsam heran
|
||||
const rows = 26;
|
||||
const flow = (t * 0.08) % 1;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const d = (r + flow) / rows; // 0 Horizont … 1 vorne
|
||||
const y = cy + (h - cy) * d ** 2.1;
|
||||
const spacing = 5 + 46 * d ** 1.6;
|
||||
const size = 0.8 + 2.4 * d;
|
||||
const a = (0.06 + 0.4 * d) * breath;
|
||||
ctx.fillStyle = rgba(PINK, a);
|
||||
const startK = -Math.ceil(cx / spacing);
|
||||
for (let k = startK; k * spacing + cx < w; k++) {
|
||||
const x = cx + k * spacing + ((r % 2) * spacing) / 2;
|
||||
const twinkle = hash(k * 13.1 + r * 7.7);
|
||||
if (twinkle < 0.18) continue;
|
||||
ctx.fillRect(x, y, size, size);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Wandlinien mit herabfließenden Daten
|
||||
for (const l of lines) {
|
||||
const top = cy - l.up;
|
||||
const len = l.up + l.down;
|
||||
const shimmer = 0.7 + 0.3 * Math.sin(t * 1.3 + l.phase);
|
||||
ctx.fillStyle = rgba(l.color, l.alpha * 0.55 * shimmer);
|
||||
ctx.fillRect(l.x, top, l.width, len);
|
||||
// heller Datenpaket-Abschnitt, der nach unten wandert
|
||||
const seg = Math.max(6, len * 0.22);
|
||||
const pos = ((t * l.flow + l.phase * 100) % (len + seg)) - seg;
|
||||
ctx.fillStyle = rgba(l.color === PINK ? HOT : l.color, l.alpha * 1.4);
|
||||
ctx.fillRect(l.x, top + Math.max(0, pos), l.width, Math.min(seg, len - Math.max(0, pos)));
|
||||
}
|
||||
|
||||
// 4) Zerfaserte Skyline (Blöcke ragen aus dem Band, tropfen nach unten)
|
||||
for (const b of blocks) {
|
||||
const x = (b.x + t * b.drift + w) % (w + b.bw) - b.bw * 0.5;
|
||||
const dist = Math.min(1, Math.abs(x + b.bw / 2 - hotX) / (w * 0.5));
|
||||
const hot = (1 - dist) ** 2;
|
||||
const a = b.alpha * (0.4 + 1 * hot) * breath;
|
||||
const y = cy - b.bh * b.lift;
|
||||
const bg = ctx.createLinearGradient(0, y, 0, y + b.bh);
|
||||
bg.addColorStop(0, rgba(PINK, a * 0.25));
|
||||
bg.addColorStop(0.6, rgba([255, 60 + 140 * hot, 120 + 100 * hot], a));
|
||||
bg.addColorStop(1, rgba(PINK, a * 0.1));
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, b.bw, b.bh);
|
||||
}
|
||||
|
||||
// 5) Lichtband: Bloom, Band, gleißender Kern; Hotspot wandert
|
||||
ctx.save();
|
||||
ctx.translate(hotX, cy);
|
||||
ctx.scale(1, 0.15);
|
||||
let rg = ctx.createRadialGradient(0, 0, 0, 0, 0, w * 0.5);
|
||||
rg.addColorStop(0, rgba(HOT, 0.6 * breath));
|
||||
rg.addColorStop(0.45, rgba(PINK, 0.22 * breath));
|
||||
rg.addColorStop(1, rgba(PINK, 0));
|
||||
ctx.fillStyle = rg;
|
||||
ctx.fillRect(-w * 1.5, -h * 4, w * 3, h * 8);
|
||||
ctx.restore();
|
||||
|
||||
g = ctx.createLinearGradient(0, cy - 10, 0, cy + 10);
|
||||
g.addColorStop(0, rgba(PINK, 0));
|
||||
g.addColorStop(0.5, rgba(PINK, 0.55 * breath));
|
||||
g.addColorStop(1, rgba(PINK, 0));
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, cy - 10, w, 20);
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(hotX, cy);
|
||||
ctx.scale(1, 0.006);
|
||||
rg = ctx.createRadialGradient(0, 0, 0, 0, 0, w * 0.55);
|
||||
rg.addColorStop(0, rgba([255, 245, 250], 1 * breath));
|
||||
rg.addColorStop(0.4, rgba(HOT, 0.8 * breath));
|
||||
rg.addColorStop(1, rgba(PINK, 0.15 * breath));
|
||||
ctx.fillStyle = rg;
|
||||
ctx.fillRect(-w * 1.5, -h * 100, w * 3, h * 200);
|
||||
ctx.restore();
|
||||
|
||||
// 6) Zeichenregen an den Rändern
|
||||
if (columns.length) {
|
||||
ctx.font = '13px ui-monospace, monospace';
|
||||
ctx.textBaseline = 'top';
|
||||
for (const c of columns) {
|
||||
const head = ((c.offset + t * c.speed) % (h + c.len * 16)) - c.len * 16;
|
||||
for (let k = 0; k < c.len; k++) {
|
||||
const y = head - k * 16;
|
||||
if (y < -16 || y > h) continue;
|
||||
const ch = GLYPHS[Math.floor(hash(c.seed * 31 + k * 7 + Math.floor(t * 3) * (k === 0 ? 1 : 0)) * GLYPHS.length)];
|
||||
ctx.fillStyle = k === 0 ? 'rgba(255,200,220,0.55)' : rgba(PINK, 0.32 * (1 - k / c.len));
|
||||
ctx.fillText(ch, c.x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7) Oben und unten ins Schwarz (dort stehen Titel und Fußzeile)
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
g = ctx.createLinearGradient(0, 0, 0, h);
|
||||
g.addColorStop(0, 'rgba(4,0,2,0.95)');
|
||||
g.addColorStop(0.22, 'rgba(4,0,2,0.5)');
|
||||
g.addColorStop(0.42, 'rgba(4,0,2,0.04)');
|
||||
g.addColorStop(0.62, 'rgba(4,0,2,0.04)');
|
||||
g.addColorStop(0.85, 'rgba(4,0,2,0.5)');
|
||||
g.addColorStop(1, 'rgba(4,0,2,0.95)');
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// 8) Glitch: Streifen seitlich versetzt
|
||||
if (!calm && now < glitchUntil) {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
for (let b = 0; b < 5; b++) {
|
||||
const sy = Math.floor((0.25 + hash(now * 0.0007 + b) * 0.5) * canvas.height);
|
||||
const sh = Math.floor((4 + hash(b + now * 0.0013) * 30) * dpr);
|
||||
const shift = Math.floor((hash(b * 3 + now * 0.0021) - 0.5) * 120 * dpr);
|
||||
ctx.drawImage(canvas, 0, sy, canvas.width, sh, shift, sy, canvas.width, sh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loop(now) {
|
||||
frame = requestAnimationFrame(loop);
|
||||
if (document.hidden || now - last < 1000 / FPS) return;
|
||||
last = now;
|
||||
if (now > nextGlitch) {
|
||||
glitchUntil = now + GLITCH_FOR;
|
||||
nextGlitch = now + GLITCH_EVERY[0] + Math.random() * (GLITCH_EVERY[1] - GLITCH_EVERY[0]);
|
||||
}
|
||||
draw(now);
|
||||
}
|
||||
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
if (!calm) frame = requestAnimationFrame(loop);
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
}, [calm]);
|
||||
|
||||
return <canvas ref={ref} className="theme-bg blackwall-field" aria-hidden="true" />;
|
||||
}
|
||||
Reference in new issue
Block a user