mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
10 KiB
JSON
1 line
10 KiB
JSON
{"name":"Particles","title":"Particles","description":"Configurable particle system.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"containerRef\" :class=\"className\" class=\"relative\"></div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';\nimport { Renderer, Camera, Geometry, Program, Mesh } from 'ogl';\n\ninterface ParticlesProps {\n particleCount?: number;\n particleSpread?: number;\n speed?: number;\n particleColors?: string[];\n moveParticlesOnHover?: boolean;\n particleHoverFactor?: number;\n alphaParticles?: boolean;\n particleBaseSize?: number;\n sizeRandomness?: number;\n cameraDistance?: number;\n disableRotation?: boolean;\n className?: string;\n}\n\nconst props = withDefaults(defineProps<ParticlesProps>(), {\n particleCount: 200,\n particleSpread: 10,\n speed: 0.1,\n particleColors: () => ['#ffffff'],\n moveParticlesOnHover: false,\n particleHoverFactor: 1,\n alphaParticles: false,\n particleBaseSize: 100,\n sizeRandomness: 1,\n cameraDistance: 20,\n disableRotation: false,\n className: ''\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst mouseRef = ref({ x: 0, y: 0 });\n\nlet renderer: Renderer | null = null;\nlet camera: Camera | null = null;\nlet particles: Mesh | null = null;\nlet program: Program | null = null;\nlet animationFrameId: number | null = null;\nlet lastTime = 0;\nlet elapsed = 0;\n\nconst defaultColors = ['#ffffff', '#ffffff', '#ffffff'];\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n hex = hex.replace(/^#/, '');\n if (hex.length === 3) {\n hex = hex\n .split('')\n .map(c => c + c)\n .join('');\n }\n const int = parseInt(hex, 16);\n const r = ((int >> 16) & 255) / 255;\n const g = ((int >> 8) & 255) / 255;\n const b = (int & 255) / 255;\n return [r, g, b];\n};\n\nconst vertex = /* glsl */ `\n attribute vec3 position;\n attribute vec4 random;\n attribute vec3 color;\n \n uniform mat4 modelMatrix;\n uniform mat4 viewMatrix;\n uniform mat4 projectionMatrix;\n uniform float uTime;\n uniform float uSpread;\n uniform float uBaseSize;\n uniform float uSizeRandomness;\n \n varying vec4 vRandom;\n varying vec3 vColor;\n \n void main() {\n vRandom = random;\n vColor = color;\n \n vec3 pos = position * uSpread;\n pos.z *= 10.0;\n \n vec4 mPos = modelMatrix * vec4(pos, 1.0);\n float t = uTime;\n mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);\n mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);\n mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);\n \n vec4 mvPos = viewMatrix * mPos;\n gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);\n gl_Position = projectionMatrix * mvPos;\n }\n`;\n\nconst fragment = /* glsl */ `\n precision highp float;\n \n uniform float uTime;\n uniform float uAlphaParticles;\n varying vec4 vRandom;\n varying vec3 vColor;\n \n void main() {\n vec2 uv = gl_PointCoord.xy;\n float d = length(uv - vec2(0.5));\n \n if(uAlphaParticles < 0.5) {\n if(d > 0.5) {\n discard;\n }\n gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0);\n } else {\n float circle = smoothstep(0.5, 0.4, d) * 0.8;\n gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);\n }\n }\n`;\n\nconst handleMouseMove = (e: MouseEvent) => {\n const container = containerRef.value;\n if (!container) return;\n\n const rect = container.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);\n mouseRef.value = { x, y };\n};\n\nconst initParticles = () => {\n const container = containerRef.value;\n if (!container) return;\n\n renderer = new Renderer({ depth: false, alpha: true });\n const gl = renderer.gl;\n container.appendChild(gl.canvas);\n gl.clearColor(0, 0, 0, 0);\n\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.display = 'block';\n gl.canvas.style.position = 'absolute';\n gl.canvas.style.top = '0';\n gl.canvas.style.left = '0';\n\n camera = new Camera(gl, { fov: 15 });\n camera.position.set(0, 0, props.cameraDistance);\n\n const resize = () => {\n if (!container) return;\n\n const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth;\n const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight;\n\n const width = Math.max(parentWidth, 300);\n const height = Math.max(parentHeight, 300);\n\n renderer!.setSize(width, height);\n camera!.perspective({ aspect: width / height });\n\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.display = 'block';\n gl.canvas.style.position = 'absolute';\n gl.canvas.style.top = '0';\n gl.canvas.style.left = '0';\n };\n window.addEventListener('resize', resize, false);\n resize();\n\n if (props.moveParticlesOnHover) {\n container.addEventListener('mousemove', handleMouseMove);\n }\n\n const count = props.particleCount;\n const positions = new Float32Array(count * 3);\n const randoms = new Float32Array(count * 4);\n const colors = new Float32Array(count * 3);\n const palette = props.particleColors && props.particleColors.length > 0 ? props.particleColors : defaultColors;\n\n for (let i = 0; i < count; i++) {\n let x: number, y: number, z: number, len: number;\n do {\n x = Math.random() * 2 - 1;\n y = Math.random() * 2 - 1;\n z = Math.random() * 2 - 1;\n len = x * x + y * y + z * z;\n } while (len > 1 || len === 0);\n const r = Math.cbrt(Math.random());\n positions.set([x * r, y * r, z * r], i * 3);\n randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);\n const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);\n colors.set(col, i * 3);\n }\n\n const geometry = new Geometry(gl, {\n position: { size: 3, data: positions },\n random: { size: 4, data: randoms },\n color: { size: 3, data: colors }\n });\n\n program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n uTime: { value: 0 },\n uSpread: { value: props.particleSpread },\n uBaseSize: { value: props.particleBaseSize },\n uSizeRandomness: { value: props.sizeRandomness },\n uAlphaParticles: { value: props.alphaParticles ? 1 : 0 }\n },\n transparent: true,\n depthTest: false\n });\n\n particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });\n\n lastTime = performance.now();\n elapsed = 0;\n\n const update = (t: number) => {\n if (!animationFrameId) return;\n animationFrameId = requestAnimationFrame(update);\n const delta = t - lastTime;\n lastTime = t;\n elapsed += delta * props.speed;\n\n if (program) {\n program.uniforms.uTime.value = elapsed * 0.001;\n program.uniforms.uSpread.value = props.particleSpread;\n program.uniforms.uBaseSize.value = props.particleBaseSize;\n program.uniforms.uSizeRandomness.value = props.sizeRandomness;\n program.uniforms.uAlphaParticles.value = props.alphaParticles ? 1 : 0;\n }\n\n if (particles) {\n if (props.moveParticlesOnHover) {\n particles.position.x = -mouseRef.value.x * props.particleHoverFactor;\n particles.position.y = -mouseRef.value.y * props.particleHoverFactor;\n } else {\n particles.position.x = 0;\n particles.position.y = 0;\n }\n\n if (!props.disableRotation) {\n particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;\n particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;\n particles.rotation.z += 0.01 * props.speed;\n }\n }\n\n if (renderer && camera && particles) {\n renderer.render({ scene: particles, camera });\n }\n };\n\n animationFrameId = requestAnimationFrame(update);\n\n return () => {\n window.removeEventListener('resize', resize);\n if (props.moveParticlesOnHover) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n animationFrameId = null;\n }\n if (container.contains(gl.canvas)) {\n container.removeChild(gl.canvas);\n }\n };\n};\n\nconst cleanup = () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n animationFrameId = null;\n }\n if (renderer) {\n const container = containerRef.value;\n const gl = renderer.gl;\n if (container && gl.canvas.parentNode === container) {\n container.removeChild(gl.canvas);\n }\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n }\n renderer = null;\n camera = null;\n particles = null;\n program = null;\n};\n\nonMounted(() => {\n initParticles();\n});\n\nonUnmounted(() => {\n cleanup();\n});\n\nwatch(\n () => [props.particleCount, props.particleColors],\n () => {\n cleanup();\n initParticles();\n },\n { deep: true }\n);\n\nwatch(\n () => [\n props.particleSpread,\n props.speed,\n props.particleBaseSize,\n props.sizeRandomness,\n props.alphaParticles,\n props.moveParticlesOnHover,\n props.particleHoverFactor,\n props.disableRotation\n ],\n () => {}\n);\n</script>\n\n<style scoped>\ndiv {\n position: absolute !important;\n top: 0 !important;\n left: 0 !important;\n width: 100% !important;\n height: 100% !important;\n}\n\n:deep(canvas) {\n position: absolute !important;\n top: 0 !important;\n left: 0 !important;\n width: 100% !important;\n height: 100% !important;\n}\n</style>\n","path":"Particles/Particles.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"ogl","version":"^1.0.11"}],"devDependencies":[],"categories":["Backgrounds"]} |