feat: Updated login bg
Server Deploy / deploy (push) Failing after 1m0s

This commit is contained in:
2026-07-24 11:02:44 +01:00
parent f0547317f7
commit accf7493e9
3 changed files with 298 additions and 4 deletions
+292
View File
@@ -0,0 +1,292 @@
"use client";
import { useEffect, useRef } from "react";
// Ambient network-traffic background for the login screen.
// Procedural node graph biased to the edges, with pulses that travel edges
// like packets. Center-right stays calm so the sign-in card reads cleanly.
interface Node {
x: number;
y: number;
r: number;
flash: number; // 0..1, brief brighten when a packet arrives
}
interface Edge {
a: number;
b: number;
len: number;
}
interface Pulse {
edge: number;
dir: 1 | -1; // travel a->b or b->a
t: number; // 0..1 progress
speed: number; // progress per second
delay: number; // seconds until it starts
life: number; // seconds since spawn
}
const BG_TOP = "#0B2A58";
const BG_BOTTOM = "#0B1120";
const NODE_COLOR = "94, 122, 168"; // steel navy, rgb parts for alpha use
const LINE_COLOR = "30, 58, 95"; // #1E3A5F
const PULSE_COLOR = "93, 202, 165"; // #5DCAA5
const PULSE_CORE = "29, 158, 117"; // #1D9E75
const MAX_DIST = 190; // connect nodes within this px
const NODE_DENSITY = 1 / 22000; // nodes per px^2
const CALM_CENTER = { cx: 0.62, cy: 0.5, rx: 0.28, ry: 0.34 }; // normalised ellipse to avoid
export function NetworkBackground() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let nodes: Node[] = [];
let edges: Edge[] = [];
let pulses: Pulse[] = [];
let width = 0;
let height = 0;
let dpr = 1;
let raf = 0;
let lastTime = 0;
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
// Distance (0..1+) into the calm zone; <1 means inside the avoid ellipse.
function calmFactor(nx: number, ny: number) {
const { cx, cy, rx, ry } = CALM_CENTER;
const dx = (nx - cx) / rx;
const dy = (ny - cy) / ry;
return Math.sqrt(dx * dx + dy * dy);
}
function build() {
nodes = [];
edges = [];
pulses = [];
const target = Math.round(width * height * NODE_DENSITY);
const count = Math.max(24, Math.min(140, target));
let guard = 0;
while (nodes.length < count && guard < count * 40) {
guard++;
// Bias toward edges: pull samples away from centre.
const ex = Math.pow(Math.random(), 0.65);
const ey = Math.pow(Math.random(), 0.65);
const nx = Math.random() < 0.5 ? ex * 0.5 : 1 - ex * 0.5;
const ny = Math.random() < 0.5 ? ey * 0.5 : 1 - ey * 0.5;
const calm = calmFactor(nx, ny);
// Reject most nodes inside the calm zone, a few survive for texture.
if (calm < 1 && Math.random() > calm * 0.35) continue;
nodes.push({
x: nx * width,
y: ny * height,
r: 2 + Math.random() * 2,
flash: 0,
});
}
// Connect nearby nodes, capped degree so it stays sparse.
const degree = new Array(nodes.length).fill(0);
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (degree[i] >= 3) break;
if (degree[j] >= 3) continue;
const dx = nodes[i].x - nodes[j].x;
const dy = nodes[i].y - nodes[j].y;
const len = Math.hypot(dx, dy);
if (len > MAX_DIST) continue;
// Skip edges that cut across the calm centre.
const mx = (nodes[i].x + nodes[j].x) / 2 / width;
const my = (nodes[i].y + nodes[j].y) / 2 / height;
if (calmFactor(mx, my) < 0.9) continue;
edges.push({ a: i, b: j, len });
degree[i]++;
degree[j]++;
}
}
// Seed one pulse per few edges, staggered.
for (let e = 0; e < edges.length; e++) {
if (Math.random() < 0.55) pulses.push(spawnPulse(e, true));
}
}
function spawnPulse(edge: number, initial = false): Pulse {
return {
edge,
dir: Math.random() < 0.5 ? 1 : -1,
t: 0,
speed: 0.18 + Math.random() * 0.22, // ~2.5-5.5s per edge
delay: (initial ? Math.random() * 8 : Math.random() * 6),
life: 0,
};
}
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
build();
}
function drawBackground() {
const g = ctx.createLinearGradient(0, 0, width, height);
g.addColorStop(0, BG_TOP);
g.addColorStop(1, BG_BOTTOM);
ctx.fillStyle = g;
ctx.fillRect(0, 0, width, height);
}
function drawStatic() {
drawBackground();
ctx.lineWidth = 1;
for (const edge of edges) {
const a = nodes[edge.a];
const b = nodes[edge.b];
ctx.strokeStyle = `rgba(${LINE_COLOR}, 0.2)`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
for (const n of nodes) {
ctx.fillStyle = `rgba(${NODE_COLOR}, 0.5)`;
ctx.beginPath();
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
ctx.fill();
}
}
function frame(now: number) {
const dt = lastTime ? Math.min((now - lastTime) / 1000, 0.05) : 0;
lastTime = now;
drawBackground();
// Edges.
ctx.lineWidth = 1;
for (const edge of edges) {
const a = nodes[edge.a];
const b = nodes[edge.b];
ctx.strokeStyle = `rgba(${LINE_COLOR}, 0.22)`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
// Pulses.
for (let i = 0; i < pulses.length; i++) {
const p = pulses[i];
p.life += dt;
if (p.life < p.delay) continue;
p.t += p.speed * dt;
const edge = edges[p.edge];
if (!edge) {
pulses[i] = spawnPulse(Math.floor(Math.random() * edges.length));
continue;
}
const a = nodes[edge.a];
const b = nodes[edge.b];
const from = p.dir === 1 ? a : b;
const to = p.dir === 1 ? b : a;
if (p.t >= 1) {
to.flash = 1; // packet arrived
pulses[i] = spawnPulse(p.edge);
continue;
}
const x = from.x + (to.x - from.x) * p.t;
const y = from.y + (to.y - from.y) * p.t;
// Fade in over first 15%, out over last 25%.
const fade = Math.min(p.t / 0.15, 1) * Math.min((1 - p.t) / 0.25, 1);
const alpha = 0.85 * fade;
ctx.save();
ctx.shadowBlur = 10;
ctx.shadowColor = `rgba(${PULSE_COLOR}, ${alpha})`;
ctx.fillStyle = `rgba(${PULSE_COLOR}, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = `rgba(${PULSE_CORE}, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, 1.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Nodes, brighter while flashing.
for (const n of nodes) {
if (n.flash > 0) n.flash = Math.max(0, n.flash - dt * 2);
const base = 0.45 + n.flash * 0.5;
if (n.flash > 0) {
ctx.save();
ctx.shadowBlur = 8 * n.flash;
ctx.shadowColor = `rgba(${PULSE_COLOR}, ${n.flash})`;
ctx.fillStyle = `rgba(${PULSE_COLOR}, ${0.4 + n.flash * 0.5})`;
ctx.beginPath();
ctx.arc(n.x, n.y, n.r + n.flash, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
} else {
ctx.fillStyle = `rgba(${NODE_COLOR}, ${base})`;
ctx.beginPath();
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
ctx.fill();
}
}
raf = requestAnimationFrame(frame);
}
function onResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resize();
if (reducedMotion) drawStatic();
}, 150);
}
resize();
if (reducedMotion) {
drawStatic();
} else {
raf = requestAnimationFrame(frame);
}
window.addEventListener("resize", onResize);
return () => {
cancelAnimationFrame(raf);
clearTimeout(resizeTimer);
window.removeEventListener("resize", onResize);
};
}, []);
return (
<canvas
ref={canvasRef}
aria-hidden="true"
className="pointer-events-none fixed inset-0 -z-10 h-full w-full"
/>
);
}