feat: 添加 InspiraUI 组件
This commit is contained in:
@ -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
53
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
294
src/pages/UI-components/InspiraUI/BubblesBg.vue
Normal file
294
src/pages/UI-components/InspiraUI/BubblesBg.vue
Normal 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>
|
72
src/pages/UI-components/InspiraUI/CardSpotlight.vue
Normal file
72
src/pages/UI-components/InspiraUI/CardSpotlight.vue
Normal 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>
|
160
src/pages/UI-components/InspiraUI/FallingStarsBg.vue
Normal file
160
src/pages/UI-components/InspiraUI/FallingStarsBg.vue
Normal 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>
|
75
src/pages/UI-components/InspiraUI/GradientButton.vue
Normal file
75
src/pages/UI-components/InspiraUI/GradientButton.vue
Normal 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>
|
26
src/pages/UI-components/InspiraUI/index.page.vue
Normal file
26
src/pages/UI-components/InspiraUI/index.page.vue
Normal 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
1
typed-router.d.ts
vendored
@ -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>>,
|
||||
|
Reference in New Issue
Block a user