feat: InspiraUI
Some checks failed
/ surge (push) Successful in 2m41s
/ build-and-deploy-to-vercel (push) Successful in 2m59s
/ lint-build-and-check (push) Has been cancelled
/ playwright (push) Has been cancelled

This commit is contained in:
严浩
2025-04-01 11:55:16 +08:00
parent db2c210a09
commit 8f2a77702b
36 changed files with 1053 additions and 372 deletions

View File

@ -0,0 +1,159 @@
<template>
<div
ref="containerRef"
class="relative size-full overflow-hidden will-change-transform"
:style="{ background }"
>
<canvas
ref="canvasRef"
class="absolute inset-0 size-full"
/>
</div>
</template>
<script setup lang="ts">
import { useRafFn, templateRef } from "@vueuse/core";
import { ref, onMounted, onBeforeUnmount } from "vue";
interface Props {
background?: string;
particleColor?: string;
minSize?: number;
maxSize?: number;
speed?: number;
particleDensity?: number;
}
interface Particle {
x: number;
y: number;
size: number;
opacity: number;
vx: number;
vy: number;
phase: number;
phaseSpeed: number;
}
const props = withDefaults(defineProps<Props>(), {
background: "#0d47a1",
particleColor: "#ffffff",
minSize: 1,
maxSize: 3,
speed: 4,
particleDensity: 120,
});
const containerRef = templateRef<HTMLElement | null>("containerRef");
const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");
const particles = ref<Particle[]>([]);
const ctx = ref<CanvasRenderingContext2D | null>(null);
// Adjust canvas size on mount and resize
function resizeCanvas() {
if (!canvasRef.value || !containerRef.value) return;
const dpr = window.devicePixelRatio || 1;
const rect = containerRef.value.getBoundingClientRect();
canvasRef.value.width = rect.width * dpr;
canvasRef.value.height = rect.height * dpr;
if (ctx.value) {
ctx.value.scale(dpr, dpr);
}
}
function generateParticles(): void {
const newParticles: Particle[] = [];
const count = props.particleDensity;
for (let i = 0; i < count; i++) {
const baseSpeed = 0.05;
const speedVariance = Math.random() * 0.3 + 0.7;
newParticles.push({
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * (props.maxSize - props.minSize) + props.minSize,
opacity: Math.random() * 0.5 + 0.3,
vx: (Math.random() - 0.5) * baseSpeed * speedVariance * props.speed,
vy: ((Math.random() - 0.5) * baseSpeed - baseSpeed * 0.3) * speedVariance * props.speed,
phase: Math.random() * Math.PI * 2,
phaseSpeed: 0.015,
});
}
particles.value = newParticles;
}
function updateAndDrawParticles() {
if (!ctx.value || !canvasRef.value) return;
const canvas = canvasRef.value;
ctx.value.clearRect(0, 0, canvas.width, canvas.height);
particles.value = particles.value.map((particle) => {
let newX = particle.x + particle.vx;
let newY = particle.y + particle.vy;
if (newX < -2) newX = 102;
if (newX > 102) newX = -2;
if (newY < -2) newY = 102;
if (newY > 102) newY = -2;
const newPhase = (particle.phase + particle.phaseSpeed) % (Math.PI * 2);
const opacity = 0.3 + (Math.sin(newPhase) * 0.3 + 0.3);
// Draw particle
ctx.value!.beginPath();
ctx.value!.arc(
(newX * canvas.width) / 100,
(newY * canvas.height) / 100,
particle.size,
0,
Math.PI * 2,
);
ctx.value!.fillStyle = `${props.particleColor}${Math.floor(opacity * 255)
.toString(16)
.padStart(2, "0")}`;
ctx.value!.fill();
return {
...particle,
x: newX,
y: newY,
phase: newPhase,
opacity,
};
});
}
const { pause, resume } = useRafFn(updateAndDrawParticles, { immediate: false });
// Handle window resize
let resizeObserver: ResizeObserver | undefined;
onMounted(() => {
if (!canvasRef.value) return;
ctx.value = canvasRef.value.getContext("2d");
resizeCanvas();
generateParticles();
// Set up resize observer
resizeObserver = new ResizeObserver(resizeCanvas);
if (containerRef.value) {
resizeObserver.observe(containerRef.value);
}
resume();
});
onBeforeUnmount(() => {
pause();
if (resizeObserver && containerRef.value) {
resizeObserver.unobserve(containerRef.value);
}
});
</script>