From 90184239c110eaaa7e7ac4d0bc7febd3cef19f4b Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Sun, 29 Mar 2026 16:43:36 +0800 Subject: [PATCH] feat(dashboard): redesign login page with reactor core aesthetic - DiamondBg: Canvas-based interactive background with: - 24px grid with faint crosshairs and recessed socket dots - 150px mouse energy field with lerp following (0.1 speed) - Crosses shrink/darken when "pressed" by energy field - Cherenkov blue core dot at exact cursor position - Gravity well glow beneath login panel - Login card: cyan edge border (rgba 0,242,255,0.2), deep blue shadow - Radial fade mask around login area - DailyQuote component replaces static welcome text --- dashboard/src/components/auth/DiamondBg.vue | 268 +++++++++++------- .../views/authentication/auth/LoginPage.vue | 16 +- 2 files changed, 180 insertions(+), 104 deletions(-) diff --git a/dashboard/src/components/auth/DiamondBg.vue b/dashboard/src/components/auth/DiamondBg.vue index 7d4ee0b5d..eca33b649 100644 --- a/dashboard/src/components/auth/DiamondBg.vue +++ b/dashboard/src/components/auth/DiamondBg.vue @@ -1,18 +1,8 @@ @@ -20,29 +10,40 @@ import { computed, onMounted, onUnmounted, ref } from "vue"; const bgEl = ref(null); -const cols = 40; -const rows = 22; -const total = computed(() => cols * rows); +const canvasEl = ref(null); +const GRID = 24; // grid cell size in px +const CROSS_SIZE = 5; // crosshair half-length +const SOCKET_RADIUS = 1.5; // recessed socket dot radius +const ENERGY_RADIUS = 150; // mouse energy field radius +const LERP = 0.1; // follow speed +const SINK_DEPTH = 0.4; // how much crosses shrink when pressed + +// mouse tracking with lerp const targetX = ref(-9999); const targetY = ref(-9999); const smoothX = ref(-9999); const smoothY = ref(-9999); -const LERP = 0.12; -const torchStyle = computed(() => ({ - "--x": `${smoothX.value}px`, - "--y": `${smoothY.value}px`, +// exact mouse for core dot +const exactX = ref(-9999); +const exactY = ref(-9999); + +const focusStyle = computed(() => ({ + left: `${exactX.value}px`, + top: `${exactY.value}px`, })); -const coreStyle = computed(() => ({ - left: `${smoothX.value}px`, - top: `${smoothY.value}px`, -})); +let ctx: CanvasRenderingContext2D | null = null; +let animId: number | null = null; +let width = 0; +let height = 0; const onMouseMove = (e: MouseEvent) => { targetX.value = e.clientX; targetY.value = e.clientY; + exactX.value = e.clientX; + exactY.value = e.clientY; }; const onMouseLeave = () => { @@ -50,39 +51,130 @@ const onMouseLeave = () => { targetY.value = -9999; }; -const getCellStyle = (idx: number) => { - const col = idx % cols; - const row = Math.floor(idx / cols); - const cellW = window.innerWidth / cols; - const cellH = window.innerHeight / rows; - const cx = col * cellW + cellW / 2; - const cy = row * cellH + cellH / 2; +function draw() { + if (!ctx || !canvasEl.value) return; - const dx = smoothX.value - cx; - const dy = smoothY.value - cy; - const dist = Math.sqrt(dx * dx + dy * dy); - const maxDist = 180; + const W = width; + const H = height; - if (dist > maxDist) { - return { opacity: "0" }; + // clear + ctx.fillStyle = "#0A0A0C"; + ctx.fillRect(0, 0, W, H); + + const cols = Math.ceil(W / GRID) + 1; + const rows = Math.ceil(H / GRID) + 1; + + const mx = smoothX.value; + const my = smoothY.value; + + // draw static base grid (very faint) + ctx.strokeStyle = "rgba(255, 255, 255, 0.03)"; + ctx.lineWidth = 0.5; + for (let x = 0; x <= W; x += GRID) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, H); + ctx.stroke(); + } + for (let y = 0; y <= H; y += GRID) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(W, y); + ctx.stroke(); } - const reveal = 1 - dist / maxDist; - return { - opacity: String(reveal * 0.4), - transform: `scale(${0.9 + reveal * 0.1})`, - }; -}; + // draw crosshairs + sockets with interaction + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const cx = c * GRID; + const cy = r * GRID; -let animId: number | null = null; -const loop = () => { - smoothX.value += (targetX.value - smoothX.value) * LERP; - smoothY.value += (targetY.value - smoothY.value) * LERP; + const dx = mx - cx; + const dy = my - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + + // socket dot at intersection (always visible, very subtle) + const socketAlpha = 0.08; + ctx.fillStyle = `rgba(5, 5, 8, ${socketAlpha})`; + ctx.beginPath(); + ctx.arc(cx, cy, SOCKET_RADIUS, 0, Math.PI * 2); + ctx.fill(); + + // crosshair + let crossAlpha = 0.05; + let crossScale = 1.0; + let crossBlue = 0; + + if (dist < ENERGY_RADIUS) { + const t = 1 - dist / ENERGY_RADIUS; + // eased + const eased = t * t * (3 - 2 * t); + crossAlpha = 0.05 + eased * 0.35; + crossScale = 1.0 - eased * SINK_DEPTH; + crossBlue = Math.floor(eased * 180); + } + + if (crossAlpha < 0.01) continue; + + const halfLen = CROSS_SIZE * crossScale; + const strokeColor = crossBlue > 0 + ? `rgba(${crossBlue * 0.3}, ${crossBlue * 0.8}, ${crossBlue}, ${crossAlpha})` + : `rgba(255, 255, 255, ${crossAlpha})`; + + ctx.strokeStyle = strokeColor; + ctx.lineWidth = 0.4; + ctx.beginPath(); + ctx.moveTo(cx - halfLen, cy); + ctx.lineTo(cx + halfLen, cy); + ctx.moveTo(cx, cy - halfLen); + ctx.lineTo(cx, cy + halfLen); + ctx.stroke(); + } + } + + // core Cherenkov dot at exact mouse position + if (mx > 0 && my > 0 && mx < W && my < H) { + const grad = ctx.createRadialGradient(mx, my, 0, mx, my, 6); + grad.addColorStop(0, "rgba(0, 242, 255, 0.95)"); + grad.addColorStop(0.3, "rgba(0, 210, 255, 0.6)"); + grad.addColorStop(1, "rgba(0, 180, 255, 0)"); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(mx, my, 6, 0, Math.PI * 2); + ctx.fill(); + } +} + +function loop() { + // lerp follow + if (targetX.value !== -9999) { + smoothX.value += (targetX.value - smoothX.value) * LERP; + smoothY.value += (targetY.value - smoothY.value) * LERP; + } + draw(); animId = requestAnimationFrame(loop); -}; +} -onMounted(() => { loop(); }); -onUnmounted(() => { if (animId) cancelAnimationFrame(animId); }); +function resize() { + if (!canvasEl.value || !bgEl.value) return; + width = window.innerWidth; + height = window.innerHeight; + canvasEl.value.width = width; + canvasEl.value.height = height; +} + +onMounted(() => { + if (!canvasEl.value) return; + ctx = canvasEl.value.getContext("2d"); + resize(); + window.addEventListener("resize", resize); + loop(); +}); + +onUnmounted(() => { + window.removeEventListener("resize", resize); + if (animId) cancelAnimationFrame(animId); +}); diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue index 095e3e7cd..f9b7b63a6 100644 --- a/dashboard/src/views/authentication/auth/LoginPage.vue +++ b/dashboard/src/views/authentication/auth/LoginPage.vue @@ -312,6 +312,17 @@ onMounted(() => { display: flex; justify-content: center; align-items: center; + // Radial fade mask around login card area + mask-image: radial-gradient( + ellipse 60% 70% at 50% 50%, + black 30%, + transparent 70% + ); + -webkit-mask-image: radial-gradient( + ellipse 60% 70% at 50% 50%, + black 30%, + transparent 70% + ); } .login-card { @@ -319,9 +330,10 @@ onMounted(() => { padding: 8px; background: rgba(6, 8, 14, 0.82) !important; backdrop-filter: blur(28px) saturate(1.1); - border: 0.5px solid rgba(200, 220, 255, 0.08); + border: 1px solid rgba(0, 242, 255, 0.2); box-shadow: - 0 0 60px rgba(0, 0, 0, 0.95), + 0 0 80px rgba(0, 26, 51, 0.95), + 0 0 120px rgba(0, 26, 51, 0.6), 0 0 0 0.5px rgba(255, 255, 255, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.02); }