feat: 添加 InspiraUI 组件
Some checks failed
/ lint-build-and-check (push) Failing after 37s
/ playwright (push) Failing after 4m50s
/ surge (push) Successful in 2m32s

This commit is contained in:
mini2024
2025-03-30 23:45:01 +08:00
parent 05d6a71da8
commit 8d0ed93ee0
8 changed files with 683 additions and 0 deletions

View File

@ -73,6 +73,7 @@
"satellite.js": "^5.0.0",
"tailwind-merge": "^3.0.2",
"tdesign-icons-vue-next": "^0.3.4",
"three": "^0.175.0",
"ts-enum-util": "^4.1.0",
"utils4u": "^4.2.1",
"vant": "^4.9.16",
@ -93,6 +94,7 @@
"@types/mockjs": "^1.0.10",
"@types/node": "^22.13.14",
"@types/nprogress": "^0.2.3",
"@types/three": "^0.175.0",
"@unocss/preset-rem-to-px": "^66.0.0",
"@vant/auto-import-resolver": "^1.2.1",
"@vitejs/plugin-vue": "^5.2.1",

53
pnpm-lock.yaml generated
View File

@ -115,6 +115,9 @@ importers:
tdesign-icons-vue-next:
specifier: ^0.3.4
version: 0.3.5(vue@3.5.13(typescript@5.8.2))
three:
specifier: ^0.175.0
version: 0.175.0
ts-enum-util:
specifier: ^4.1.0
version: 4.1.0
@ -170,6 +173,9 @@ importers:
'@types/nprogress':
specifier: ^0.2.3
version: 0.2.3
'@types/three':
specifier: ^0.175.0
version: 0.175.0
'@unocss/preset-rem-to-px':
specifier: ^66.0.0
version: 66.0.0
@ -1386,6 +1392,9 @@ packages:
'@tsconfig/node22@22.0.1':
resolution: {integrity: sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
'@tweenjs/tween.js@25.0.0':
resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==}
@ -1440,6 +1449,12 @@ packages:
'@types/readdir-glob@1.1.5':
resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==}
'@types/stats.js@0.17.3':
resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
'@types/three@0.175.0':
resolution: {integrity: sha512-ldMSBgtZOZ3g9kJ3kOZSEtZIEITmJOzu8eKVpkhf036GuNkM4mt0NXecrjCn5tMm1OblOF7dZehlaDypBfNokw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@ -1449,6 +1464,9 @@ packages:
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@types/webxr@0.5.21':
resolution: {integrity: sha512-geZIAtLzjGmgY2JUi6VxXdCrTb99A7yP49lxLr2Nm/uIK0PkkxcEi4OGhoGDO4pxCf3JwGz2GiJL2Ej4K2bKaA==}
'@typescript-eslint/eslint-plugin@8.28.0':
resolution: {integrity: sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -2015,6 +2033,9 @@ packages:
peerDependencies:
vue: ^3.5.0
'@webgpu/types@0.1.60':
resolution: {integrity: sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==}
'@zip.js/zip.js@2.7.57':
resolution: {integrity: sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==}
engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'}
@ -2912,6 +2933,9 @@ packages:
picomatch:
optional: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@ -3669,6 +3693,9 @@ packages:
mersenne-twister@1.1.0:
resolution: {integrity: sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==}
meshoptimizer@0.18.1:
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
meshoptimizer@0.22.0:
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
@ -4708,6 +4735,9 @@ packages:
text-decoder@1.2.3:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
three@0.175.0:
resolution: {integrity: sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==}
throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
@ -6282,6 +6312,8 @@ snapshots:
'@tsconfig/node22@22.0.1': {}
'@tweenjs/tween.js@23.1.3': {}
'@tweenjs/tween.js@25.0.0': {}
'@tybys/wasm-util@0.9.0':
@ -6333,6 +6365,17 @@ snapshots:
dependencies:
'@types/node': 22.13.14
'@types/stats.js@0.17.3': {}
'@types/three@0.175.0':
dependencies:
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.3
'@types/webxr': 0.5.21
'@webgpu/types': 0.1.60
fflate: 0.8.2
meshoptimizer: 0.18.1
'@types/trusted-types@2.0.7':
optional: true
@ -6340,6 +6383,8 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@types/webxr@0.5.21': {}
'@typescript-eslint/eslint-plugin@8.28.0(@typescript-eslint/parser@8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@ -7129,6 +7174,8 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.8.2)
'@webgpu/types@0.1.60': {}
'@zip.js/zip.js@2.7.57': {}
abbrev@2.0.0: {}
@ -8163,6 +8210,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fflate@0.8.2: {}
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@ -8895,6 +8944,8 @@ snapshots:
mersenne-twister@1.1.0: {}
meshoptimizer@0.18.1: {}
meshoptimizer@0.22.0: {}
micromatch@4.0.8:
@ -9971,6 +10022,8 @@ snapshots:
dependencies:
b4a: 1.6.7
three@0.175.0: {}
throttle-debounce@5.0.2: {}
through@2.3.8: {}

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>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import { useLayout } from '@/layouts/sakai-vue/composables/layout';
import { computed } from 'vue';
import CardSpotlight from './CardSpotlight.vue';
import GradientButton from './GradientButton.vue';
const { isDarkTheme } = useLayout();
// const isDark = computed(() => useColorMode().value == 'dark');
const bgColor = computed(() => (isDarkTheme.value ? '#000' : '#fff'));
</script>
<template>
<div class="z-10 flex h-56 w-full flex-col items-center justify-center">
<GradientButton :bg-color="bgColor">Zooooooooooom 🚀</GradientButton>
</div>
<div class="flex h-[500px] w-full flex-col gap-4 lg:h-[250px] lg:flex-row">
<CardSpotlight
class="cursor-pointer flex-col items-center justify-center whitespace-nowrap text-4xl shadow-2xl"
:gradient-color="isDarkTheme ? '#363636' : '#C9C9C9'"
>
Card Spotlight
</CardSpotlight>
</div>
</template>

1
typed-router.d.ts vendored
View File

@ -37,6 +37,7 @@ declare module 'vue-router/auto-routes' {
'UIComponentsComponents': RouteRecordInfo<'UIComponentsComponents', '/UI-components/Components', Record<never, never>, Record<never, never>>,
'UIComponentsInfiniteLoading': RouteRecordInfo<'UIComponentsInfiniteLoading', '/UI-components/infinite-loading', Record<never, never>, Record<never, never>>,
'UIComponentsInfiniteLoadingDetail': RouteRecordInfo<'UIComponentsInfiniteLoadingDetail', '/UI-components/infinite-loading/detail', Record<never, never>, Record<never, never>>,
'UIComponentsInspiraUI': RouteRecordInfo<'UIComponentsInspiraUI', '/UI-components/InspiraUI', Record<never, never>, Record<never, never>>,
'UIComponentsPrimeVue': RouteRecordInfo<'UIComponentsPrimeVue', '/UI-components/PrimeVue', Record<never, never>, Record<never, never>>,
'UIComponentsShadcnVue': RouteRecordInfo<'UIComponentsShadcnVue', '/UI-components/ShadcnVue', Record<never, never>, Record<never, never>>,
'VueMacrosDefineRender': RouteRecordInfo<'VueMacrosDefineRender', '/VueMacros/DefineRender', Record<never, never>, Record<never, never>>,