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);
}