mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
12 KiB
JSON
1 line
12 KiB
JSON
{"name":"LightPillar","title":"LightPillar","description":"Vertical pillar of light with glow effects.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport * as THREE from 'three';\nimport {\n onBeforeMount,\n onBeforeUnmount,\n onMounted,\n ref,\n shallowRef,\n useTemplateRef,\n watch,\n type CSSProperties\n} from 'vue';\n\ninterface LightPillarProps {\n topColor?: string;\n bottomColor?: string;\n intensity?: number;\n rotationSpeed?: number;\n interactive?: boolean;\n className?: string;\n glowAmount?: number;\n pillarWidth?: number;\n pillarHeight?: number;\n noiseIntensity?: number;\n mixBlendMode?: CSSProperties['mixBlendMode'];\n pillarRotation?: number;\n}\n\nconst props = withDefaults(defineProps<LightPillarProps>(), {\n topColor: '#48FF28',\n bottomColor: '#9EF19E',\n intensity: 1.0,\n rotationSpeed: 0.3,\n interactive: false,\n className: '',\n glowAmount: 0.005,\n pillarWidth: 3.0,\n pillarHeight: 0.4,\n noiseIntensity: 0.5,\n mixBlendMode: 'screen',\n pillarRotation: 0\n});\n\nconst containerRef = useTemplateRef('containerRef');\nconst rafRef = ref<number | null>(null);\nconst rendererRef = shallowRef<THREE.WebGLRenderer | null>(null);\nconst materialRef = shallowRef<THREE.ShaderMaterial | null>(null);\nconst sceneRef = shallowRef<THREE.Scene | null>(null);\nconst cameraRef = shallowRef<THREE.OrthographicCamera | null>(null);\nconst geometryRef = shallowRef<THREE.PlaneGeometry | null>(null);\nconst mouseRef = ref<THREE.Vector2>(new THREE.Vector2(0, 0));\nconst timeRef = ref<number>(0);\nconst webGLSupported = ref<boolean>(true);\n\nonBeforeMount(() => {\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n\n if (!gl) {\n webGLSupported.value = false;\n console.warn('WebGL is not supported in this browser');\n }\n\n canvas.remove();\n});\n\nlet cleanup: (() => void) | null = null;\nconst setup = () => {\n if (!containerRef.value || !webGLSupported.value) return;\n\n const container = containerRef.value;\n const width = container.clientWidth;\n const height = container.clientHeight;\n\n // Scene setup\n const scene = new THREE.Scene();\n sceneRef.value = scene;\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n cameraRef.value = camera;\n\n let renderer: THREE.WebGLRenderer;\n try {\n renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true,\n powerPreference: 'high-performance',\n precision: 'lowp',\n stencil: false,\n depth: false\n });\n } catch (error) {\n console.error('Failed to create WebGL renderer:', error);\n webGLSupported.value = false;\n return;\n }\n\n renderer.setSize(width, height);\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n container.appendChild(renderer.domElement);\n rendererRef.value = renderer;\n\n // Convert hex colors to RGB\n const parseColor = (hex: string): THREE.Vector3 => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n\n // Shader material\n const vertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float uTime;\n uniform vec2 uResolution;\n uniform vec2 uMouse;\n uniform vec3 uTopColor;\n uniform vec3 uBottomColor;\n uniform float uIntensity;\n uniform bool uInteractive;\n uniform float uGlowAmount;\n uniform float uPillarWidth;\n uniform float uPillarHeight;\n uniform float uNoiseIntensity;\n uniform float uPillarRotation;\n varying vec2 vUv;\n\n const float PI = 3.141592653589793;\n const float EPSILON = 0.001;\n const float E = 2.71828182845904523536;\n const float HALF = 0.5;\n\n mat2 rot(float angle) {\n float s = sin(angle);\n float c = cos(angle);\n return mat2(c, -s, s, c);\n }\n\n // Procedural noise function\n float noise(vec2 coord) {\n float G = E;\n vec2 r = (G * sin(G * coord));\n return fract(r.x * r.y * (1.0 + coord.x));\n }\n\n // Apply layered wave deformation to position\n vec3 applyWaveDeformation(vec3 pos, float timeOffset) {\n float frequency = 1.0;\n float amplitude = 1.0;\n vec3 deformed = pos;\n\n for(float i = 0.0; i < 4.0; i++) {\n deformed.xz *= rot(0.4);\n float phase = timeOffset * i * 2.0;\n vec3 oscillation = cos(deformed.zxy * frequency - phase);\n deformed += oscillation * amplitude;\n frequency *= 2.0;\n amplitude *= HALF;\n }\n return deformed;\n }\n\n // Polynomial smooth blending between two values\n float blendMin(float a, float b, float k) {\n float scaledK = k * 4.0;\n float h = max(scaledK - abs(a - b), 0.0);\n return min(a, b) - h * h * 0.25 / scaledK;\n }\n\n float blendMax(float a, float b, float k) {\n return -blendMin(-a, -b, k);\n }\n\n void main() {\n vec2 fragCoord = vUv * uResolution;\n vec2 uv = (fragCoord * 2.0 - uResolution) / uResolution.y;\n\n // Apply 2D rotation to UV coordinates\n float rotAngle = uPillarRotation * PI / 180.0;\n uv *= rot(rotAngle);\n\n vec3 origin = vec3(0.0, 0.0, -10.0);\n vec3 direction = normalize(vec3(uv, 1.0));\n\n float maxDepth = 50.0;\n float depth = 0.1;\n\n mat2 rotX = rot(uTime * 0.3);\n if(uInteractive && length(uMouse) > 0.0) {\n rotX = rot(uMouse.x * PI * 2.0);\n }\n\n vec3 color = vec3(0.0);\n\n for(float i = 0.0; i < 100.0; i++) {\n vec3 pos = origin + direction * depth;\n pos.xz *= rotX;\n\n // Apply vertical scaling and wave deformation\n vec3 deformed = pos;\n deformed.y *= uPillarHeight;\n deformed = applyWaveDeformation(deformed + vec3(0.0, uTime, 0.0), uTime);\n\n // Calculate distance field using cosine pattern\n vec2 cosinePair = cos(deformed.xz);\n float fieldDistance = length(cosinePair) - 0.2;\n\n // Radial boundary constraint\n float radialBound = length(pos.xz) - uPillarWidth;\n fieldDistance = blendMax(radialBound, fieldDistance, 1.0);\n fieldDistance = abs(fieldDistance) * 0.15 + 0.01;\n\n vec3 gradient = mix(uBottomColor, uTopColor, smoothstep(15.0, -15.0, pos.y));\n color += gradient * pow(1.0 / fieldDistance, 1.0);\n\n if(fieldDistance < EPSILON || depth > maxDepth) break;\n depth += fieldDistance;\n }\n\n // Normalize by pillar width to maintain consistent glow regardless of size\n float widthNormalization = uPillarWidth / 3.0;\n color = tanh(color * uGlowAmount / widthNormalization);\n\n // Add noise postprocessing\n float rnd = noise(gl_FragCoord.xy);\n color -= rnd / 15.0 * uNoiseIntensity;\n\n gl_FragColor = vec4(color * uIntensity, 1.0);\n }\n `;\n\n const material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new THREE.Vector2(width, height) },\n uMouse: { value: mouseRef.value },\n uTopColor: { value: parseColor(props.topColor) },\n uBottomColor: { value: parseColor(props.bottomColor) },\n uIntensity: { value: props.intensity },\n uInteractive: { value: props.interactive },\n uGlowAmount: { value: props.glowAmount },\n uPillarWidth: { value: props.pillarWidth },\n uPillarHeight: { value: props.pillarHeight },\n uNoiseIntensity: { value: props.noiseIntensity },\n uPillarRotation: { value: props.pillarRotation }\n },\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.value = material;\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n geometryRef.value = geometry;\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n // Mouse interaction - throttled for performance\n let mouseMoveTimeout: number | null = null;\n const handleMouseMove = (event: MouseEvent) => {\n if (!props.interactive) return;\n\n if (mouseMoveTimeout) return;\n\n mouseMoveTimeout = window.setTimeout(() => {\n mouseMoveTimeout = null;\n }, 16); // ~60fps throttle\n\n const rect = container.getBoundingClientRect();\n const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;\n mouseRef.value.set(x, y);\n };\n\n if (props.interactive) {\n container.addEventListener('mousemove', handleMouseMove, { passive: true });\n }\n\n // Animation loop with fixed timestep\n let lastTime = performance.now();\n const targetFPS = 60;\n const frameTime = 1000 / targetFPS;\n\n const animate = (currentTime: number) => {\n if (!materialRef.value || !rendererRef.value || !sceneRef.value || !cameraRef.value) return;\n\n const deltaTime = currentTime - lastTime;\n\n if (deltaTime >= frameTime) {\n timeRef.value += 0.016 * props.rotationSpeed;\n materialRef.value.uniforms.uTime.value = timeRef.value;\n rendererRef.value.render(sceneRef.value, cameraRef.value);\n lastTime = currentTime - (deltaTime % frameTime);\n }\n\n rafRef.value = requestAnimationFrame(animate);\n };\n rafRef.value = requestAnimationFrame(animate);\n\n // Handle resize with debouncing\n let resizeTimeout: number | null = null;\n const handleResize = () => {\n if (resizeTimeout) {\n clearTimeout(resizeTimeout);\n }\n\n resizeTimeout = window.setTimeout(() => {\n if (!rendererRef.value || !materialRef.value || !containerRef.value) return;\n const newWidth = containerRef.value.clientWidth;\n const newHeight = containerRef.value.clientHeight;\n rendererRef.value.setSize(newWidth, newHeight);\n materialRef.value.uniforms.uResolution.value.set(newWidth, newHeight);\n }, 150);\n };\n\n window.addEventListener('resize', handleResize, { passive: true });\n\n // Cleanup\n cleanup = () => {\n window.removeEventListener('resize', handleResize);\n if (props.interactive) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n if (rafRef.value) {\n cancelAnimationFrame(rafRef.value);\n }\n if (rendererRef.value) {\n rendererRef.value.dispose();\n rendererRef.value.forceContextLoss();\n if (container.contains(rendererRef.value.domElement)) {\n container.removeChild(rendererRef.value.domElement);\n }\n }\n if (materialRef.value) {\n materialRef.value.dispose();\n }\n if (geometryRef.value) {\n geometryRef.value.dispose();\n }\n\n rendererRef.value = null;\n materialRef.value = null;\n sceneRef.value = null;\n cameraRef.value = null;\n geometryRef.value = null;\n rafRef.value = null;\n };\n};\n\nonMounted(() => {\n setup();\n});\n\nonBeforeUnmount(() => {\n cleanup?.();\n});\n\nwatch(\n () => [\n props.topColor,\n props.bottomColor,\n props.intensity,\n props.rotationSpeed,\n props.interactive,\n props.glowAmount,\n props.pillarWidth,\n props.pillarHeight,\n props.noiseIntensity,\n props.pillarRotation,\n webGLSupported.value\n ],\n () => {\n cleanup?.();\n setup();\n },\n {\n deep: true\n }\n);\n</script>\n\n<template>\n <div\n v-if=\"!webGLSupported\"\n :class=\"`w-full h-full absolute top-0 left-0 flex items-center justify-center bg-black/10 text-gray-500 text-sm ${className}`\"\n :style=\"{ mixBlendMode }\"\n >\n WebGL not supported\n </div>\n <div\n v-else\n ref=\"containerRef\"\n :class=\"`w-full h-full absolute top-0 left-0 ${className}`\"\n :style=\"{ mixBlendMode }\"\n />\n</template>\n","path":"LightPillar/LightPillar.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"three","version":"^0.178.0"}],"devDependencies":[],"categories":["Backgrounds"]} |