From accf7493e9fd155759fa7d1162b248f0b2599220 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 24 Jul 2026 11:02:44 +0100 Subject: [PATCH] feat: Updated login bg --- web/app/login/page.tsx | 8 +- web/components/NetworkBackground.tsx | 292 +++++++++++++++++++++++++++ web/tsconfig.tsbuildinfo | 2 +- 3 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 web/components/NetworkBackground.tsx diff --git a/web/app/login/page.tsx b/web/app/login/page.tsx index af10142..6268a30 100644 --- a/web/app/login/page.tsx +++ b/web/app/login/page.tsx @@ -5,6 +5,7 @@ import { useMutation } from "@tanstack/react-query"; import { auth } from "@/lib/api"; import { Button, Card } from "@/components/ui"; import { Logo } from "@/components/Logo"; +import { NetworkBackground } from "@/components/NetworkBackground"; export default function LoginPage() { const [email, setEmail] = useState(""); @@ -45,11 +46,12 @@ export default function LoginPage() { } return ( -
-
+
+ +
-

Sign in to Vantage

+

Sign in to Vantage

Organisation: {orgName}

diff --git a/web/components/NetworkBackground.tsx b/web/components/NetworkBackground.tsx new file mode 100644 index 0000000..4e8e555 --- /dev/null +++ b/web/components/NetworkBackground.tsx @@ -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(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 | 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 ( +