feat: 更新依赖
Some checks failed
/ lint-build-and-check (push) Failing after 38s
/ build-and-deploy-to-vercel (push) Successful in 3m4s
/ playwright (push) Failing after 5m32s
/ surge (push) Successful in 2m18s

This commit is contained in:
mini2024
2025-03-31 00:37:57 +08:00
parent 8d0ed93ee0
commit 0cc1730b2c
9 changed files with 382 additions and 390 deletions

View File

@ -0,0 +1,294 @@
<script setup lang="ts">
import {
Clock,
Color,
MathUtils,
Mesh,
PerspectiveCamera,
Scene,
ShaderMaterial,
SphereGeometry,
Vector3,
WebGLRenderer,
} from 'three';
import { onBeforeUnmount, onMounted, ref } from 'vue';
defineProps({
blur: {
default: 0,
type: Number,
},
});
const bubbleParentContainer = ref<HTMLElement | null>(null);
const bubbleCanvasContainer = ref<HTMLElement | null>(null);
let renderer: WebGLRenderer;
let scene: Scene;
let camera: PerspectiveCamera;
let clock: Clock;
const spheres: Mesh[] = [];
const BG_COLOR_BOTTOM_BLUISH = rgb(170, 215, 217);
const BG_COLOR_TOP_BLUISH = rgb(57, 167, 255);
const BG_COLOR_BOTTOM_ORANGISH = rgb(255, 160, 75);
const BG_COLOR_TOP_ORANGISH = rgb(239, 172, 53);
const SPHERE_COLOR_BOTTOM_BLUISH = rgb(120, 235, 124);
const SPHERE_COLOR_TOP_BLUISH = rgb(0, 167, 255);
const SPHERE_COLOR_BOTTOM_ORANGISH = rgb(235, 170, 0);
const SPHERE_COLOR_TOP_ORANGISH = rgb(255, 120, 0);
const SPHERE_COUNT = 250;
const SPHERE_SCALE_COEFF = 3;
const ORBIT_MIN = SPHERE_SCALE_COEFF + 2;
const ORBIT_MAX = ORBIT_MIN + 10;
const RAND_SEED = 898_211_544;
const rand = seededRandom(RAND_SEED);
const { cos, PI, sin } = Math;
const PI2 = PI * 2;
const sizes = new Array(SPHERE_COUNT).fill(0).map(() => randRange(1) * Math.pow(randRange(), 3));
const orbitRadii = new Array(SPHERE_COUNT).fill(0).map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
const positions: [number, number, number][] = orbitRadii.map((rad, i) => [
rad * cos(thetas[i]) * sin(phis[i]),
rad * sin(thetas[i]) * sin(phis[i]),
rad * cos(phis[i]),
]);
const sphereGeometry = new SphereGeometry(SPHERE_SCALE_COEFF);
const sphereMaterial = getGradientMaterial(
SPHERE_COLOR_BOTTOM_BLUISH,
SPHERE_COLOR_TOP_BLUISH,
SPHERE_COLOR_BOTTOM_ORANGISH,
SPHERE_COLOR_TOP_ORANGISH,
);
const bgGeometry = new SphereGeometry();
bgGeometry.scale(-1, 1, 1);
const bgMaterial = getGradientMaterial(
BG_COLOR_BOTTOM_BLUISH,
BG_COLOR_TOP_BLUISH,
BG_COLOR_BOTTOM_ORANGISH,
BG_COLOR_TOP_ORANGISH,
);
bgMaterial.uniforms.uTemperatureVariancePeriod.value = new Vector3(0, 0, 0.1);
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
const temperature = sin(elapsed * 0.5) * 0.5 + 0.5;
bgMaterial.uniforms.uTemperature.value = temperature;
bgMaterial.uniforms.uElapsedTime.value = elapsed;
sphereMaterial.uniforms.uTemperature.value = temperature;
sphereMaterial.uniforms.uElapsedTime.value = elapsed;
// Floating effect for spheres
for (const [index, sphere] of spheres.entries()) {
const basePosition = positions[index];
const floatFactor = 2; // Adjust this value to control float intensity
const speed = 0.3; // Adjust this value to control float speed
const floatY = sin(elapsed * speed + index) * floatFactor;
sphere.position.y = basePosition[1] + floatY;
}
renderer.render(scene, camera);
}
function createScene() {
const width = bubbleCanvasContainer.value?.clientWidth || 1;
const height = bubbleCanvasContainer.value?.clientHeight || 1;
// Set up the scene, camera, and renderer
scene = new Scene();
camera = new PerspectiveCamera(50, width / height, 1, 2000);
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 23;
renderer = new WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setClearColor(BG_COLOR_BOTTOM_BLUISH);
// Add these properties to allow overlap
sphereMaterial.depthWrite = false;
sphereMaterial.depthTest = true; // Keep this true for depth sorting
if (bubbleCanvasContainer.value) {
bubbleCanvasContainer.value.append(renderer.domElement);
}
// Create the background mesh
const bgMesh = new Mesh(bgGeometry, bgMaterial);
// Position the background far behind everything
bgMesh.position.set(0, 0, -1); // Move the background far back
// Disable depth testing for the background to ensure it's always behind other objects
bgMesh.material.depthTest = false;
bgMesh.renderOrder = -1; // Ensure the background is rendered first
// Calculate the scale to ensure the background covers the full canvas
const distance = camera.position.z; // Distance from the camera
const aspect = camera.aspect;
const frustumHeight = 2 * distance * Math.tan(MathUtils.degToRad(camera.fov) / 2);
const frustumWidth = frustumHeight * aspect;
// Scale the background geometry to match the camera's frustum size
bgMesh.scale.set(frustumWidth / bgGeometry.parameters.radius, frustumHeight / bgGeometry.parameters.radius, 1);
scene.add(bgMesh); // Add the backgrou
// Create sphere meshes
const orbitRadii = new Array(SPHERE_COUNT).fill(0).map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
const positions = orbitRadii.map((rad, i) => [
rad * cos(thetas[i]) * sin(phis[i]),
rad * sin(thetas[i]) * sin(phis[i]),
rad * cos(phis[i]),
]);
for (let i = 0; i < SPHERE_COUNT; i++) {
const sphere = new Mesh(sphereGeometry, sphereMaterial);
const [x, y, z] = positions[i];
const scaleVector = sizes[i];
sphere.scale.set(scaleVector, scaleVector, scaleVector);
sphere.position.set(x, y, z);
spheres.push(sphere);
scene.add(sphere);
}
clock = new Clock();
}
function getGradientMaterial(colorBottomWarm: Color, colorTopWarm: Color, colorBottomCool: Color, colorTopCool: Color) {
return new ShaderMaterial({
fragmentShader: `
uniform vec3 colorBottomWarm;
uniform vec3 colorTopWarm;
uniform vec3 colorBottomCool;
uniform vec3 colorTopCool;
varying float topBottomMix;
varying float warmCoolMix;
void main() {
gl_FragColor = vec4(mix(
mix(colorTopCool, colorTopWarm, warmCoolMix),
mix(colorBottomCool, colorBottomWarm, warmCoolMix),
topBottomMix), 1.0);
}
`,
uniforms: {
colorBottomCool: {
value: new Color().copy(colorBottomCool),
},
colorBottomWarm: {
value: new Color().copy(colorBottomWarm),
},
colorTopCool: {
value: new Color().copy(colorTopCool),
},
colorTopWarm: {
value: new Color().copy(colorTopWarm),
},
uElapsedTime: {
value: 0,
},
uTemperature: {
value: 0,
},
uTemperatureVariancePeriod: {
value: new Vector3(0.08, 0.1, 0.2),
},
},
vertexShader: `
uniform vec4 uTemperatureVariancePeriod;
uniform float uTemperature;
uniform float uElapsedTime;
varying float topBottomMix;
varying float warmCoolMix;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
topBottomMix = normal.y;
warmCoolMix = 0.6 * uTemperature +
0.4 * (sin(
(uElapsedTime + gl_Position.x) * uTemperatureVariancePeriod.x +
(uElapsedTime + gl_Position.y) * uTemperatureVariancePeriod.y +
(uElapsedTime + gl_Position.z) * uTemperatureVariancePeriod.z) * 0.5 + 0.5);
}
`,
});
}
function randRange(n = 1) {
return rand() * n;
}
function rgb(r: number, g: number, b: number) {
return new Color(r / 255, g / 255, b / 255);
}
function seededRandom(a: number) {
return function () {
a = Math.trunc(a);
a = Math.trunc(a + 0x9e_37_79_b9);
let t = a ^ (a >>> 16);
t = Math.imul(t, 0x21_f0_aa_ad);
t = t ^ (t >>> 15);
t = Math.imul(t, 0x73_5a_2d_97);
return ((t = t ^ (t >>> 15)) >>> 0) / 4_294_967_296;
};
}
function updateRendererSize() {
const width = bubbleParentContainer.value?.clientWidth || 1;
const height = bubbleParentContainer.value?.clientHeight || 1;
// Update renderer size and aspect ratio
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// Recalculate background mesh scale
const distance = camera.position.z;
const frustumHeight = 2 * distance * Math.tan(MathUtils.degToRad(camera.fov) / 2);
const frustumWidth = frustumHeight * camera.aspect;
// Get the background mesh and update its scale
const bgMesh = scene.children.find((obj) => obj instanceof Mesh && obj.geometry === bgGeometry) as Mesh;
if (bgMesh) {
bgMesh.scale.set(frustumWidth / bgGeometry.parameters.radius, frustumHeight / bgGeometry.parameters.radius, 1);
}
}
onMounted(() => {
createScene();
updateRendererSize();
window.addEventListener('resize', updateRendererSize);
animate();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateRendererSize); // Cleanup on component unmount
});
</script>
<template>
<div ref="bubbleParentContainer" class="relative h-72 w-full overflow-hidden">
<div ref="bubbleCanvasContainer"></div>
<div
:style="{
'--bubbles-blur': `${blur}px`,
}"
class="absolute inset-0 z-[2] size-full backdrop-blur-[--bubbles-blur]"
>
<slot />
</div>
</div>
</template>

View File

@ -0,0 +1,72 @@
<!-- https://inspira-ui.com/components/cards/card-spotlight -->
<script setup lang="ts">
import { cn } from '@/shadcn/lib/utils';
import { computed, type HTMLAttributes, onMounted, ref } from 'vue';
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class'];
gradientColor?: string;
gradientOpacity?: number;
gradientSize?: number;
slotClass?: HTMLAttributes['class'];
}>(),
{
class: '',
gradientColor: '#262626',
gradientOpacity: 0.8,
gradientSize: 200,
slotClass: '',
},
);
const mouseX = ref(-props.gradientSize * 10);
const mouseY = ref(-props.gradientSize * 10);
function handleMouseLeave() {
mouseX.value = -props.gradientSize * 10;
mouseY.value = -props.gradientSize * 10;
}
function handleMouseMove(e: MouseEvent) {
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
mouseX.value = e.clientX - rect.left;
mouseY.value = e.clientY - rect.top;
}
onMounted(() => {
mouseX.value = -props.gradientSize * 10;
mouseY.value = -props.gradientSize * 10;
});
const backgroundStyle = computed(() => {
return `radial-gradient(
circle at ${mouseX.value}px ${mouseY.value}px,
${props.gradientColor} 0%,
rgba(0, 0, 0, 0) 70%
)`;
});
</script>
<template>
<div
:class="[
'group relative flex size-full overflow-hidden rounded-xl border bg-neutral-100 text-black dark:bg-neutral-900 dark:text-white',
$props.class,
]"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div :class="cn('relative z-10', props.slotClass)">
<slot></slot>
</div>
<div
class="pointer-events-none absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
:style="{
background: backgroundStyle,
opacity: gradientOpacity,
}"
></div>
</div>
</template>

View File

@ -0,0 +1,160 @@
<script setup lang="ts">
import { cn } from '@/shadcn/lib/utils';
import { onMounted, ref } from 'vue';
interface Star {
speed: number;
x: number;
y: number;
z: number;
}
const props = withDefaults(
defineProps<{
class?: string;
color?: string;
count?: number;
}>(),
{
color: '#FFF',
count: 200,
},
);
const starsCanvas = ref<HTMLCanvasElement | null>(null);
let perspective: number = 0;
let stars: Star[] = [];
let ctx: CanvasRenderingContext2D | null = null;
onMounted(() => {
const canvas = starsCanvas.value;
if (!canvas) return;
window.addEventListener('resize', resizeCanvas);
resizeCanvas(); // Call it initially to set correct size
perspective = canvas.width / 2;
stars = [];
// Initialize stars
for (let i = 0; i < props.count; i++) {
stars.push({
speed: Math.random() * 5 + 2, // Speed for falling effect
x: (Math.random() - 0.5) * 2 * canvas.width,
y: (Math.random() - 0.5) * 2 * canvas.height,
z: Math.random() * canvas.width,
});
}
animate(); // Start animation
});
// Function to animate the stars
function animate() {
const canvas = starsCanvas.value;
if (!canvas) return;
ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas for each frame
for (const star of stars) {
drawStar(star);
// Move star towards the screen (decrease z)
star.z -= star.speed;
// Reset star when it reaches the viewer (z = 0)
if (star.z <= 0) {
star.z = canvas.width;
star.x = (Math.random() - 0.5) * 2 * canvas.width;
star.y = (Math.random() - 0.5) * 2 * canvas.height;
}
}
requestAnimationFrame(animate); // Continue animation
}
// Function to draw a star with a sharp line and blurred trail
function drawStar(star: Star) {
const canvas = starsCanvas.value;
if (!canvas) return;
ctx = canvas.getContext('2d');
if (!ctx) return;
const scale = perspective / (perspective + star.z); // 3D perspective scale
const x2d = canvas.width / 2 + star.x * scale;
const y2d = canvas.height / 2 + star.y * scale;
const size = Math.max(scale * 3, 0.5); // Size based on perspective
// Previous position for a trail effect
const prevScale = perspective / (perspective + star.z + star.speed * 15); // Longer trail distance
const xPrev = canvas.width / 2 + star.x * prevScale;
const yPrev = canvas.height / 2 + star.y * prevScale;
const rgb = hexToRgb();
// Draw blurred trail (longer, with low opacity)
ctx.save(); // Save current context state for restoring later
ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.2)`;
ctx.lineWidth = size * 2.5; // Thicker trail for a blur effect
ctx.shadowBlur = 35; // Add blur to the trail
ctx.shadowColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.8)`;
ctx.beginPath();
ctx.moveTo(x2d, y2d);
ctx.lineTo(xPrev, yPrev); // Longer trail
ctx.stroke();
ctx.restore(); // Restore context state to remove blur from the main line
// Draw sharp line (no blur)
ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.6)`;
ctx.lineWidth = size; // The line width is the same as the star's size
ctx.beginPath();
ctx.moveTo(x2d, y2d);
ctx.lineTo(xPrev, yPrev); // Sharp trail
ctx.stroke();
// Draw the actual star (dot)
ctx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`;
ctx.beginPath();
ctx.arc(x2d, y2d, size / 4, 0, Math.PI * 2); // Dot with size matching the width
ctx.fill();
}
function hexToRgb() {
let hex = props.color.replace(/^#/, '');
// If the hex code is 3 characters, expand it to 6 characters
if (hex.length === 3) {
hex = [...hex].map((char) => char + char).join('');
}
// Parse the r, g, b values from the hex string
const bigint = Number.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 {
b,
g,
r,
};
}
// Set canvas to full screen
function resizeCanvas() {
const canvas = starsCanvas.value;
if (!canvas) return;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
</script>
<template>
<canvas ref="starsCanvas" :class="cn('absolute inset-0 w-full h-full', $props.class)"></canvas>
</template>

View File

@ -0,0 +1,75 @@
<!-- https://inspira-ui.com/components/buttons/gradient-button -->
<script lang="ts" setup>
import { cn } from '@/shadcn/lib/utils';
import { computed } from 'vue';
interface GradientButtonProps {
bgColor?: string;
blur?: number;
borderRadius?: number;
borderWidth?: number;
class?: string;
colors?: string[];
duration?: number;
}
const props = withDefaults(defineProps<GradientButtonProps>(), {
bgColor: '#000',
blur: 4,
borderRadius: 8,
borderWidth: 2,
colors: () => ['#FF0000', '#FFA500', '#FFFF00', '#008000', '#0000FF', '#4B0082', '#EE82EE', '#FF0000'],
duration: 2500,
});
const durationInMilliseconds = computed(() => `${props.duration}ms`);
const allColors = computed(() => props.colors.join(', '));
const borderWidthInPx = computed(() => `${props.borderWidth}px`);
const borderRadiusInPx = computed(() => `${props.borderRadius}px`);
const blurPx = computed(() => `${props.blur}px`);
</script>
<template>
<button
:class="
cn(
'relative flex items-center justify-center min-w-28 min-h-10 overflow-hidden before:absolute before:-inset-[200%] animate-rainbow rainbow-btn',
props.class,
)
"
>
<span class="btn-content inline-flex size-full items-center justify-center px-4 py-2">
<slot />
</span>
</button>
</template>
<style scoped>
.animate-rainbow::before {
content: '';
background: conic-gradient(v-bind(allColors));
animation: rotate-rainbow v-bind(durationInMilliseconds) linear infinite;
filter: blur(v-bind(blurPx));
padding: v-bind(borderWidthInPx);
}
.rainbow-btn {
padding: v-bind(borderWidthInPx);
border-radius: v-bind(borderRadiusInPx);
}
.btn-content {
border-radius: v-bind(borderRadiusInPx);
background-color: v-bind(bgColor);
z-index: 0;
}
@keyframes rotate-rainbow {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>