mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat(dashboard): introduce reactor core glassmorphism theme
Global reactor background (ReactorBg): - Full-screen canvas with 24px crosshair grid and socket dots - Mouse energy field (120px radius) with lerp=0.08 smooth tracking - Crosses shrink and glow cyan when energy field passes - Canvas sits behind all UI (z-index 0, pointer-events none) Theme updates: - Dark surface now #0F0F12 for reactor aesthetic - Background deepens to #0A0A0C - Cards: translucent glassmorphism (40% opacity, blur 12px) with inset shadow, 1px cyan border on hover - Sidebar: glassmorphism (75% opacity, blur 16px, cyan border) - Header: glassmorphism (80% opacity, blur 20px, cyan border) - V-main: transparent so reactor bg shows through
This commit is contained in:
159
dashboard/src/components/shared/ReactorBg.vue
Normal file
159
dashboard/src/components/shared/ReactorBg.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<canvas ref="canvasEl" class="reactor-bg-canvas" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { useReactorBg } from "@/composables/useReactorBg";
|
||||
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null);
|
||||
const { mouseX, mouseY, onMouseMove, onMouseLeave } = useReactorBg();
|
||||
|
||||
const GRID = 24;
|
||||
const CROSS_SIZE = 4;
|
||||
const SOCKET_RADIUS = 1.2;
|
||||
const ENERGY_RADIUS = 120;
|
||||
const LERP = 0.08;
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let animId: number | null = null;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
let smoothX = -9999;
|
||||
let smoothY = -9999;
|
||||
let targetX = -9999;
|
||||
let targetY = -9999;
|
||||
|
||||
function draw() {
|
||||
if (!ctx || !canvasEl.value) return;
|
||||
|
||||
const W = width;
|
||||
const H = height;
|
||||
|
||||
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;
|
||||
const my = smoothY;
|
||||
|
||||
// faint base grid
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.025)";
|
||||
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();
|
||||
}
|
||||
|
||||
// crosshairs + sockets with energy interaction
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const cx = c * GRID;
|
||||
const cy = r * GRID;
|
||||
|
||||
const dx = mx - cx;
|
||||
const dy = my - cy;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// socket dot
|
||||
ctx.fillStyle = "rgba(5, 5, 8, 0.1)";
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, SOCKET_RADIUS, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
let crossAlpha = 0.04;
|
||||
let crossScale = 1.0;
|
||||
let crossBlue = 0;
|
||||
|
||||
if (dist < ENERGY_RADIUS) {
|
||||
const t = 1 - dist / ENERGY_RADIUS;
|
||||
const eased = t * t * (3 - 2 * t);
|
||||
crossAlpha = 0.04 + eased * 0.4;
|
||||
crossScale = 1.0 - eased * 0.35;
|
||||
crossBlue = Math.floor(eased * 200);
|
||||
}
|
||||
|
||||
if (crossAlpha < 0.01) continue;
|
||||
|
||||
const halfLen = CROSS_SIZE * crossScale;
|
||||
const strokeColor = crossBlue > 0
|
||||
? `rgba(${Math.floor(crossBlue * 0.3)}, ${Math.floor(crossBlue * 0.85)}, ${crossBlue}, ${crossAlpha})`
|
||||
: `rgba(255, 255, 255, ${crossAlpha})`;
|
||||
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = 0.35;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - halfLen, cy);
|
||||
ctx.lineTo(cx + halfLen, cy);
|
||||
ctx.moveTo(cx, cy - halfLen);
|
||||
ctx.lineTo(cx, cy + halfLen);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loop() {
|
||||
if (targetX !== -9999) {
|
||||
smoothX += (targetX - smoothX) * LERP;
|
||||
smoothY += (targetY - smoothY) * LERP;
|
||||
}
|
||||
// Sync from global mouse tracking
|
||||
if (mouseX.value !== -9999) {
|
||||
targetX = mouseX.value;
|
||||
targetY = mouseY.value;
|
||||
} else {
|
||||
targetX = -9999;
|
||||
targetY = -9999;
|
||||
}
|
||||
draw();
|
||||
animId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
if (!canvasEl.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);
|
||||
// Track global mouse
|
||||
window.addEventListener("mousemove", onMouseMove as EventListener);
|
||||
window.addEventListener("mouseleave", onMouseLeave);
|
||||
loop();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", resize);
|
||||
window.removeEventListener("mousemove", onMouseMove as EventListener);
|
||||
window.removeEventListener("mouseleave", onMouseLeave);
|
||||
if (animId) cancelAnimationFrame(animId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reactor-bg-canvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
36
dashboard/src/composables/useReactorBg.ts
Normal file
36
dashboard/src/composables/useReactorBg.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Global reactor background mouse tracking
|
||||
* Shares mouse position across all components so the canvas
|
||||
* background can react to UI element proximity.
|
||||
*/
|
||||
import { ref, readonly } from "vue";
|
||||
|
||||
const mouseX = ref(-9999);
|
||||
const mouseY = ref(-9999);
|
||||
const isOverUI = ref(false);
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
mouseX.value = e.clientX;
|
||||
mouseY.value = e.clientY;
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
mouseX.value = -9999;
|
||||
mouseY.value = -9999;
|
||||
isOverUI.value = false;
|
||||
}
|
||||
|
||||
function setIsOverUI(val: boolean) {
|
||||
isOverUI.value = val;
|
||||
}
|
||||
|
||||
export function useReactorBg() {
|
||||
return {
|
||||
mouseX: readonly(mouseX),
|
||||
mouseY: readonly(mouseY),
|
||||
isOverUI: readonly(isOverUI),
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
setIsOverUI,
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import VerticalHeaderVue from "./vertical-header/VerticalHeader.vue";
|
||||
import MigrationDialog from "@/components/shared/MigrationDialog.vue";
|
||||
import ReadmeDialog from "@/components/shared/ReadmeDialog.vue";
|
||||
import Chat from "@/components/chat/Chat.vue";
|
||||
import ReactorBg from "@/components/shared/ReactorBg.vue";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useRouterLoadingStore } from "@/stores/routerLoading";
|
||||
import { useI18n } from "@/i18n/composables";
|
||||
@@ -108,6 +109,7 @@ onMounted(() => {
|
||||
customizer.inputBg ? 'inputWithbg' : '',
|
||||
]"
|
||||
>
|
||||
<ReactorBg />
|
||||
<v-progress-linear
|
||||
v-if="routerLoadingStore.isLoading"
|
||||
:model-value="routerLoadingStore.progress"
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
// App bar / header glassmorphism
|
||||
.v-app-bar {
|
||||
background: rgba(15, 15, 18, 0.8) !important;
|
||||
backdrop-filter: blur(20px) saturate(1.3) !important;
|
||||
border-bottom: 1px solid rgba(0, 242, 255, 0.08) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// V-main background (let the reactor bg show through)
|
||||
.v-main {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
html {
|
||||
.bg-success {
|
||||
color: white !important;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
// Outline Card
|
||||
.v-card--variant-outlined {
|
||||
border-color: rgba(var(--v-theme-borderLight), 0.36);
|
||||
border-color: rgba(0, 242, 255, 0.12);
|
||||
.v-divider {
|
||||
border-color: rgba(var(--v-theme-borderLight), 0.36);
|
||||
border-color: rgba(0, 242, 255, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,20 @@
|
||||
.v-card {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
border-radius: 24px;
|
||||
background: rgba(20, 20, 25, 0.4) !important;
|
||||
backdrop-filter: blur(12px) saturate(1.1);
|
||||
border: 1px solid rgba(0, 242, 255, 0.1);
|
||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5) !important;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 242, 255, 0.35);
|
||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5), 0 0 30px rgba(0, 242, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
&.withbg {
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
background-color: rgba(10, 10, 12, 0.6) !important;
|
||||
}
|
||||
&.overflow-hidden {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
/*This is for the logo*/
|
||||
.leftSidebar {
|
||||
border: 0px;
|
||||
border-right: 1px solid rgba(var(--v-theme-borderLight), 0.45);
|
||||
border-right: 1px solid rgba(0, 242, 255, 0.1) !important;
|
||||
box-shadow: none !important;
|
||||
background: rgba(15, 15, 18, 0.75) !important;
|
||||
backdrop-filter: blur(16px) saturate(1.2);
|
||||
}
|
||||
.listitem {
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -43,9 +43,9 @@ const BlueBusinessDarkTheme: ThemeTypes = {
|
||||
"on-error-container": "#FFDAD6", // Light text on container
|
||||
|
||||
// === MD3 Surface Colors (Dark Mode) ===
|
||||
surface: "#141218", // Very dark purple-gray background
|
||||
surface: "#0F0F12", // Very dark background for reactor aesthetic
|
||||
"on-surface": "#E4E1E6", // Light text on dark surface
|
||||
"surface-variant": "#43444E", // Elevated dark surface
|
||||
"surface-variant": "#1A1A22", // Elevated dark surface (glassmorphism layer)
|
||||
"on-surface-variant": "#C4C6D0", // Light text on variant
|
||||
surfaceTint: "#A1C9FF", // Blue tint for elevation
|
||||
|
||||
@@ -59,7 +59,7 @@ const BlueBusinessDarkTheme: ThemeTypes = {
|
||||
"inverse-primary": "#005FB0", // Dark primary on light
|
||||
|
||||
// === Additional UI Colors ===
|
||||
background: "#141218", // Same as surface
|
||||
background: "#0A0A0C", // Deep reactor black
|
||||
accent: "#FFAB91", // Peach accent
|
||||
|
||||
// === Light Variant Colors (inverted for dark mode) ===
|
||||
@@ -79,8 +79,8 @@ const BlueBusinessDarkTheme: ThemeTypes = {
|
||||
inputBorder: "#8E9099", // Input borders
|
||||
|
||||
// === Container/Card Colors ===
|
||||
containerBg: "#1A1A1F", // Dark card backgrounds
|
||||
"on-surface-variant-bg": "#1E1E24", // Slightly lighter background
|
||||
containerBg: "rgba(20, 20, 25, 0.4)", // Translucent glassmorphism cards
|
||||
"on-surface-variant-bg": "rgba(26, 26, 34, 0.5)", // Slightly lighter glassmorphism
|
||||
|
||||
// === Social Colors ===
|
||||
facebook: "#5388D4",
|
||||
|
||||
Reference in New Issue
Block a user