Merge pull request #13 from coolxitech/main

新增特效背景并优化Docker容器大小
This commit is contained in:
谦君
2025-12-17 23:16:11 +08:00
committed by GitHub
11 changed files with 626 additions and 22 deletions

View File

@@ -1,15 +1,16 @@
FROM node:latest AS builder FROM node:lts-alpine AS builder
RUN mkdir -p /workspace RUN mkdir -p /workspace
WORKDIR /workspace WORKDIR /workspace
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk update && apk add git
RUN npm config set registry https://registry.npmmirror.com RUN npm config set registry https://registry.npmmirror.com
RUN git clone https://github.com/setube/ogame-vue-ts.git RUN git clone https://github.com/setube/ogame-vue-ts.git
RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/ RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/
RUN npm install -g pnpm ; pnpm install; RUN npm install -g pnpm ; pnpm install;
RUN pnpm build RUN pnpm run build
# --- 第二阶段Nginx ---
FROM nginx:alpine FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf

View File

@@ -23,18 +23,16 @@
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"finalhandler": "^2.1.1", "finalhandler": "^2.1.1",
"lucide-vue-next": "^0.556.0", "lucide-vue-next": "^0.556.0",
"marked": "^17.0.1", "marked": "^17.0.1",
"motion-v": "^1.7.4",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"reka-ui": "^2.6.1", "reka-ui": "^2.6.1",
"serve-static": "^2.2.0", "serve-static": "^2.2.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "4", "vue-router": "4",
@@ -46,10 +44,13 @@
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"electron": "^39.2.7", "electron": "^39.2.7",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.2.5", "vite": "npm:rolldown-vite@7.2.5",

76
pnpm-lock.yaml generated
View File

@@ -20,12 +20,6 @@ importers:
'@vueuse/core': '@vueuse/core':
specifier: ^14.1.0 specifier: ^14.1.0
version: 14.1.0(vue@3.5.25(typescript@5.9.3)) version: 14.1.0(vue@3.5.25(typescript@5.9.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
crypto-js: crypto-js:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
@@ -41,6 +35,9 @@ importers:
marked: marked:
specifier: ^17.0.1 specifier: ^17.0.1
version: 17.0.1 version: 17.0.1
motion-v:
specifier: ^1.7.4
version: 1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
pinia: pinia:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
@@ -53,9 +50,6 @@ importers:
serve-static: serve-static:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
tailwindcss: tailwindcss:
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17 version: 4.1.17
@@ -84,6 +78,12 @@ importers:
'@vue/tsconfig': '@vue/tsconfig':
specifier: ^0.8.1 specifier: ^0.8.1
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
cross-env: cross-env:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3 version: 7.0.3
@@ -96,6 +96,9 @@ importers:
electron-vite: electron-vite:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)) version: 5.0.0(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
tw-animate-css: tw-animate-css:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
@@ -2103,6 +2106,20 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fresh@2.0.0: fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -2244,6 +2261,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hey-listen@1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -2772,6 +2792,18 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
motion-v@1.7.4:
resolution: {integrity: sha512-YNDUAsany04wfI7YtHxQK3kxzNvh+OdFUk9GpA3+hMt7j6P+5WrVAAgr8kmPPoVza9EsJiAVhqoN3YYFN0Twrw==}
peerDependencies:
'@vueuse/core': '>=10.0.0'
vue: '>=3.0.0'
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -6027,6 +6059,12 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
framer-motion@12.23.12:
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
fresh@2.0.0: {} fresh@2.0.0: {}
fs-extra@10.1.0: fs-extra@10.1.0:
@@ -6208,6 +6246,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hey-listen@1.0.8: {}
hookable@5.5.3: {} hookable@5.5.3: {}
hosted-git-info@4.1.0: hosted-git-info@4.1.0:
@@ -6694,6 +6734,24 @@ snapshots:
mkdirp@1.0.4: {} mkdirp@1.0.4: {}
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
motion-v@1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.25(typescript@5.9.3))
framer-motion: 12.23.12
hey-listen: 1.0.8
motion-dom: 12.23.12
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- '@emotion/is-prop-valid'
- react
- react-dom
ms@2.1.3: {} ms@2.1.3: {}
muggle-string@0.4.1: {} muggle-string@0.4.1: {}

View File

@@ -327,9 +327,30 @@
<!-- 内容区域 --> <!-- 内容区域 -->
<main class="flex-1 overflow-y-auto"> <main class="flex-1 overflow-y-auto">
<div class="animate-fade-in"> <Transition name="page" mode="out-in">
<RouterView /> <div :key="$route.fullPath" class="h-full">
<StarsBackground
v-if="isDark"
:factor="0.05"
:speed="50"
star-color="#fff"
class="h-full"
>
<RouterView class="h-full" />
</StarsBackground>
<div v-else class="h-full">
<ParticlesBg
class="absolute inset-0"
:quantity="100"
:ease="100"
:color="isDark ? '#FFF' : '#000'"
:staticity="10"
refresh
/>
<RouterView class="h-full" />
</div> </div>
</div>
</Transition>
</main> </main>
</div> </div>
</SidebarInset> </SidebarInset>
@@ -453,6 +474,8 @@
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { migrateGameData } from '@/utils/migration' import { migrateGameData } from '@/utils/migration'
import { checkLatestVersion } from '@/utils/versionCheck' import { checkLatestVersion } from '@/utils/versionCheck'
import {StarsBackground} from "@/components/ui/bg-stars";
import {ParticlesBg} from "@/components/ui/particles-bg";
// 执行数据迁移(在 store 初始化之前) // 执行数据迁移(在 store 初始化之前)
migrateGameData() migrateGameData()

92
src/assets/main.css Normal file
View File

@@ -0,0 +1,92 @@
@custom-variant dark (&:is(.dark *));
:root {
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.141 0.005 285.823);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
html {
color-scheme: light dark;
}
html.dark {
color-scheme: dark;
}
html.light {
color-scheme: light;
}

View File

@@ -0,0 +1,176 @@
<template>
<div
:class="
cn(
'relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]',
props.class,
)
"
@mousemove="handleMouseMove"
>
<motion.div :style="{ x: springX, y: springY }">
<!-- Star Layer 1 -->
<motion.div
class="absolute top-0 left-0 w-full h-[2000px]"
:animate="{ y: [0, -2000] }"
:transition="starLayer1Transition"
>
<div
class="absolute bg-transparent rounded-full"
:style="{
width: '1px',
height: '1px',
boxShadow: boxShadow1,
}"
/>
<div
class="absolute bg-transparent rounded-full top-[2000px]"
:style="{
width: '1px',
height: '1px',
boxShadow: boxShadow1,
}"
/>
</motion.div>
<!-- Star Layer 2 -->
<motion.div
class="absolute top-0 left-0 w-full h-[2000px]"
:animate="{ y: [0, -2000] }"
:transition="starLayer2Transition"
>
<div
class="absolute bg-transparent rounded-full"
:style="{
width: '2px',
height: '2px',
boxShadow: boxShadow2,
}"
/>
<div
class="absolute bg-transparent rounded-full top-[2000px]"
:style="{
width: '2px',
height: '2px',
boxShadow: boxShadow2,
}"
/>
</motion.div>
<!-- Star Layer 3 -->
<motion.div
class="absolute top-0 left-0 w-full h-[2000px]"
:animate="{ y: [0, -2000] }"
:transition="starLayer3Transition"
>
<div
class="absolute bg-transparent rounded-full"
:style="{
width: '3px',
height: '3px',
boxShadow: boxShadow3,
}"
/>
<div
class="absolute bg-transparent rounded-full top-[2000px]"
:style="{
width: '3px',
height: '3px',
boxShadow: boxShadow3,
}"
/>
</motion.div>
</motion.div>
<!-- Slot for child content -->
<slot />
</div>
</template>
<script setup lang="ts">
import type {SpringOptions, Transition} from "motion-v";
import { cn } from "@/lib/utils";
import { motion, useMotionValue, useSpring } from "motion-v";
import { computed, onMounted, ref, watch } from "vue";
interface StarsBackgroundProps {
factor?: number;
speed?: number;
transition?: SpringOptions;
starColor?: string;
class?: string;
}
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
factor: 0.05,
speed: 50,
transition: () => ({ stiffness: 50, damping: 20 }),
starColor: "#fff",
});
// For slot content
defineSlots();
function generateStars(count: number, starColor: string) {
const shadows: string[] = [];
for (let i = 0; i < count; i++) {
const x = Math.floor(Math.random() * 4000) - 2000;
const y = Math.floor(Math.random() * 4000) - 2000;
shadows.push(`${x}px ${y}px ${starColor}`);
}
return shadows.join(", ");
}
const offsetX = useMotionValue(1);
const offsetY = useMotionValue(1);
const springX = useSpring(offsetX, props.transition);
const springY = useSpring(offsetY, props.transition);
function handleMouseMove(e: MouseEvent) {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const newOffsetX = -(e.clientX - centerX) * props.factor;
const newOffsetY = -(e.clientY - centerY) * props.factor;
offsetX.set(newOffsetX);
offsetY.set(newOffsetY);
}
const boxShadow1 = ref("");
const boxShadow2 = ref("");
const boxShadow3 = ref("");
onMounted(() => {
boxShadow1.value = generateStars(1000, props.starColor);
boxShadow2.value = generateStars(400, props.starColor);
boxShadow3.value = generateStars(200, props.starColor);
});
// Watch for starColor changes
watch(
() => props.starColor,
(newColor) => {
boxShadow1.value = generateStars(1000, newColor);
boxShadow2.value = generateStars(400, newColor);
boxShadow3.value = generateStars(200, newColor);
},
);
const starLayer1Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed,
ease: "linear" as const,
}));
const starLayer2Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed * 2,
ease: "linear" as const,
}));
const starLayer3Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed * 3,
ease: "linear" as const,
}));
</script>

View File

@@ -0,0 +1 @@
export { default as StarsBackground } from "./StarsBackground.vue";

View File

@@ -0,0 +1,250 @@
<template>
<div
ref="canvasContainerRef"
:class="$props.class"
aria-hidden="true"
>
<canvas ref="canvasRef"></canvas>
</div>
</template>
<script setup lang="ts">
import { useMouse, useDevicePixelRatio } from "@vueuse/core";
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from "vue";
type Circle = {
x: number;
y: number;
translateX: number;
translateY: number;
size: number;
alpha: number;
targetAlpha: number;
dx: number;
dy: number;
magnetism: number;
};
type Props = {
color?: string;
quantity?: number;
staticity?: number;
ease?: number;
class?: string;
};
const props = withDefaults(defineProps<Props>(), {
color: "#FFF",
quantity: 100,
staticity: 50,
ease: 50,
class: "",
});
const canvasRef = ref<HTMLCanvasElement | null>(null);
const canvasContainerRef = ref<HTMLDivElement | null>(null);
const context = ref<CanvasRenderingContext2D | null>(null);
const circles = ref<Circle[]>([]);
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 });
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 });
const { x: mouseX, y: mouseY } = useMouse();
const { pixelRatio } = useDevicePixelRatio();
const color = computed(() => {
// Remove the leading '#' if it's present
let hex = props.color.replace(/^#/, "");
// If the hex code is 3 characters, expand it to 6 characters
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => char + char)
.join("");
}
// Parse the r, g, b values from the hex string
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255; // Extract the red component
const g = (bigint >> 8) & 255; // Extract the green component
const b = bigint & 255; // Extract the blue component
// Return the RGB values as a string separated by spaces
return `${r} ${g} ${b}`;
});
onMounted(() => {
if (canvasRef.value) {
context.value = canvasRef.value.getContext("2d");
}
initCanvas();
animate();
window.addEventListener("resize", initCanvas);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", initCanvas);
});
watch([mouseX, mouseY], () => {
onMouseMove();
});
function initCanvas() {
resizeCanvas();
drawParticles();
}
function onMouseMove() {
if (canvasRef.value) {
const rect = canvasRef.value.getBoundingClientRect();
const { w, h } = canvasSize;
const x = mouseX.value - rect.left - w / 2;
const y = mouseY.value - rect.top - h / 2;
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
if (inside) {
mouse.x = x;
mouse.y = y;
}
}
}
function resizeCanvas() {
if (canvasContainerRef.value && canvasRef.value && context.value) {
circles.value.length = 0;
canvasSize.w = canvasContainerRef.value.offsetWidth;
canvasSize.h = canvasContainerRef.value.offsetHeight;
canvasRef.value.width = canvasSize.w * pixelRatio.value;
canvasRef.value.height = canvasSize.h * pixelRatio.value;
canvasRef.value.style.width = canvasSize.w + "px";
canvasRef.value.style.height = canvasSize.h + "px";
context.value.scale(pixelRatio.value, pixelRatio.value);
}
}
function circleParams(): Circle {
const x = Math.floor(Math.random() * canvasSize.w);
const y = Math.floor(Math.random() * canvasSize.h);
const translateX = 0;
const translateY = 0;
const size = Math.floor(Math.random() * 2) + 1;
const alpha = 0;
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
const dx = (Math.random() - 0.5) * 0.2;
const dy = (Math.random() - 0.5) * 0.2;
const magnetism = 0.1 + Math.random() * 4;
return {
x,
y,
translateX,
translateY,
size,
alpha,
targetAlpha,
dx,
dy,
magnetism,
};
}
function drawCircle(circle: Circle, update = false) {
if (context.value) {
const { x, y, translateX, translateY, size, alpha } = circle;
context.value.translate(translateX, translateY);
context.value.beginPath();
context.value.arc(x, y, size, 0, 2 * Math.PI);
context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`;
context.value.fill();
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0);
if (!update) {
circles.value.push(circle);
}
}
}
function clearContext() {
if (context.value) {
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h);
}
}
function drawParticles() {
clearContext();
const particleCount = props.quantity;
for (let i = 0; i < particleCount; i++) {
const circle = circleParams();
drawCircle(circle);
}
}
function remapValue(
value: number,
start1: number,
end1: number,
start2: number,
end2: number,
): number {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
}
function animate() {
clearContext();
circles.value.forEach((circle, i) => {
// Handle the alpha value
const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
if (remapClosestEdge > 1) {
circle.alpha += 0.02;
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
}
circle.x += circle.dx;
circle.y += circle.dy;
circle.translateX +=
(mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease;
circle.translateY +=
(mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease;
// circle gets out of the canvas
if (
circle.x < -circle.size ||
circle.x > canvasSize.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.h + circle.size
) {
// remove the circle from the array
circles.value.splice(i, 1);
// create a new circle
const newCircle = circleParams();
drawCircle(newCircle);
// update the circle position
} else {
drawCircle(
{
...circle,
x: circle.x,
y: circle.y,
translateX: circle.translateX,
translateY: circle.translateY,
alpha: circle.alpha,
},
true,
);
}
});
window.requestAnimationFrame(animate);
}
</script>

View File

@@ -0,0 +1 @@
export { default as ParticlesBg } from "./ParticlesBg.vue";

View File

@@ -1,7 +1,7 @@
import type { ClassValue } from 'clsx' import type { ClassValue } from "clsx"
import { clsx } from 'clsx' import { clsx } from "clsx"
import { twMerge } from 'tailwind-merge' import { twMerge } from "tailwind-merge"
export const cn = (...inputs: ClassValue[]) => { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }

View File

@@ -1,5 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "@/assets/main.css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));