mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
15 KiB
JSON
1 line
15 KiB
JSON
{"name":"FloatingLines","title":"FloatingLines","description":"3D floating lines that react to cursor movement.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport {\n Clock,\n Mesh,\n OrthographicCamera,\n PlaneGeometry,\n Scene,\n ShaderMaterial,\n Vector2,\n Vector3,\n WebGLRenderer\n} from 'three';\nimport { onBeforeUnmount, onMounted, ref, useTemplateRef, watch, type CSSProperties } from 'vue';\n\nconst vertexShader = `\nprecision highp float;\n\nvoid main() {\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform float iTime;\nuniform vec3 iResolution;\nuniform float animationSpeed;\n\nuniform bool enableTop;\nuniform bool enableMiddle;\nuniform bool enableBottom;\n\nuniform int topLineCount;\nuniform int middleLineCount;\nuniform int bottomLineCount;\n\nuniform float topLineDistance;\nuniform float middleLineDistance;\nuniform float bottomLineDistance;\n\nuniform vec3 topWavePosition;\nuniform vec3 middleWavePosition;\nuniform vec3 bottomWavePosition;\n\nuniform vec2 iMouse;\nuniform bool interactive;\nuniform float bendRadius;\nuniform float bendStrength;\nuniform float bendInfluence;\n\nuniform bool parallax;\nuniform float parallaxStrength;\nuniform vec2 parallaxOffset;\n\nuniform vec3 lineGradient[8];\nuniform int lineGradientCount;\n\nconst vec3 BLACK = vec3(0.0);\nconst vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;\nconst vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;\n\nmat2 rotate(float r) {\n return mat2(cos(r), sin(r), -sin(r), cos(r));\n}\n\nvec3 background_color(vec2 uv) {\n vec3 col = vec3(0.0);\n\n float y = sin(uv.x - 0.2) * 0.3 - 0.1;\n float m = uv.y - y;\n\n col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));\n col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));\n return col * 0.5;\n}\n\nvec3 getLineColor(float t, vec3 baseColor) {\n if (lineGradientCount <= 0) {\n return baseColor;\n }\n\n vec3 gradientColor;\n\n if (lineGradientCount == 1) {\n gradientColor = lineGradient[0];\n } else {\n float clampedT = clamp(t, 0.0, 0.9999);\n float scaled = clampedT * float(lineGradientCount - 1);\n int idx = int(floor(scaled));\n float f = fract(scaled);\n int idx2 = min(idx + 1, lineGradientCount - 1);\n\n vec3 c1 = lineGradient[idx];\n vec3 c2 = lineGradient[idx2];\n\n gradientColor = mix(c1, c2, f);\n }\n\n return gradientColor * 0.5;\n}\n\nfloat wave(vec2 uv, float offset, vec2 screenUv, vec2 mouseUv, bool shouldBend) {\n float time = iTime * animationSpeed;\n\n float x_offset = offset;\n float x_movement = time * 0.1;\n float amp = sin(offset + time * 0.2) * 0.3;\n float y = sin(uv.x + x_offset + x_movement) * amp;\n\n if (shouldBend) {\n vec2 d = screenUv - mouseUv;\n float influence = exp(-dot(d, d) * bendRadius);\n float bendOffset = (mouseUv.y - screenUv.y) * influence * bendStrength * bendInfluence;\n y += bendOffset;\n }\n\n float m = uv.y - y;\n return 0.0175 / max(abs(m) + 0.01, 1e-3) + 0.01;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;\n baseUv.y *= -1.0;\n\n if (parallax) {\n baseUv += parallaxOffset;\n }\n\n vec3 col = vec3(0.0);\n\n vec3 b = lineGradientCount > 0 ? vec3(0.0) : background_color(baseUv);\n\n vec2 mouseUv = vec2(0.0);\n if (interactive) {\n mouseUv = (2.0 * iMouse - iResolution.xy) / iResolution.y;\n mouseUv.y *= -1.0;\n }\n\n if (enableBottom) {\n for (int i = 0; i < bottomLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(bottomLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n\n float angle = bottomWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(bottomLineDistance * fi + bottomWavePosition.x, bottomWavePosition.y),\n 1.5 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.2;\n }\n }\n\n if (enableMiddle) {\n for (int i = 0; i < middleLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(middleLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n\n float angle = middleWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n col += lineCol * wave(\n ruv + vec2(middleLineDistance * fi + middleWavePosition.x, middleWavePosition.y),\n 2.0 + 0.15 * fi,\n baseUv,\n mouseUv,\n interactive\n );\n }\n }\n\n if (enableTop) {\n for (int i = 0; i < topLineCount; ++i) {\n float fi = float(i);\n float t = fi / max(float(topLineCount - 1), 1.0);\n vec3 lineCol = getLineColor(t, b);\n\n float angle = topWavePosition.z * log(length(baseUv) + 1.0);\n vec2 ruv = baseUv * rotate(angle);\n ruv.x *= -1.0;\n col += lineCol * wave(\n ruv + vec2(topLineDistance * fi + topWavePosition.x, topWavePosition.y),\n 1.0 + 0.2 * fi,\n baseUv,\n mouseUv,\n interactive\n ) * 0.1;\n }\n }\n\n fragColor = vec4(col, 1.0);\n}\n\nvoid main() {\n vec4 color = vec4(0.0);\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}\n`;\n\nconst MAX_GRADIENT_STOPS = 8;\n\ntype WavePosition = {\n x: number;\n y: number;\n rotate: number;\n};\n\ntype FloatingLinesProps = {\n linesGradient?: string[];\n enabledWaves?: Array<'top' | 'middle' | 'bottom'>;\n lineCount?: number | number[];\n lineDistance?: number | number[];\n topWavePosition?: WavePosition;\n middleWavePosition?: WavePosition;\n bottomWavePosition?: WavePosition;\n animationSpeed?: number;\n interactive?: boolean;\n bendRadius?: number;\n bendStrength?: number;\n mouseDamping?: number;\n parallax?: boolean;\n parallaxStrength?: number;\n mixBlendMode?: CSSProperties['mixBlendMode'];\n};\n\nconst props = withDefaults(defineProps<FloatingLinesProps>(), {\n enabledWaves: () => ['top', 'middle', 'bottom'],\n lineCount: () => [6],\n lineDistance: () => [5],\n bottomWavePosition: () => ({ x: 2.0, y: -0.7, rotate: -1 }),\n animationSpeed: 1,\n interactive: true,\n bendRadius: 5.0,\n bendStrength: -0.5,\n mouseDamping: 0.05,\n parallax: true,\n parallaxStrength: 0.2,\n mixBlendMode: 'screen'\n});\n\nfunction hexToVec3(hex: string): Vector3 {\n let value = hex.trim();\n\n if (value.startsWith('#')) {\n value = value.slice(1);\n }\n\n let r = 255;\n let g = 255;\n let b = 255;\n\n if (value.length === 3) {\n r = parseInt(value[0] + value[0], 16);\n g = parseInt(value[1] + value[1], 16);\n b = parseInt(value[2] + value[2], 16);\n } else if (value.length === 6) {\n r = parseInt(value.slice(0, 2), 16);\n g = parseInt(value.slice(2, 4), 16);\n b = parseInt(value.slice(4, 6), 16);\n }\n\n return new Vector3(r / 255, g / 255, b / 255);\n}\n\nconst containerRef = useTemplateRef('containerRef');\nconst targetMouseRef = ref<Vector2>(new Vector2(-1000, -1000));\nconst currentMouseRef = ref<Vector2>(new Vector2(-1000, -1000));\nconst targetInfluenceRef = ref<number>(0);\nconst currentInfluenceRef = ref<number>(0);\nconst targetParallaxRef = ref<Vector2>(new Vector2(0, 0));\nconst currentParallaxRef = ref<Vector2>(new Vector2(0, 0));\n\nlet cleanup: (() => void) | null = null;\nconst setup = () => {\n if (!containerRef.value) return;\n\n const getLineCount = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof props.lineCount === 'number') return props.lineCount;\n if (!props.enabledWaves.includes(waveType)) return 0;\n const index = props.enabledWaves.indexOf(waveType);\n return props.lineCount[index] ?? 6;\n };\n\n const getLineDistance = (waveType: 'top' | 'middle' | 'bottom'): number => {\n if (typeof props.lineDistance === 'number') return props.lineDistance;\n if (!props.enabledWaves.includes(waveType)) return 0.1;\n const index = props.enabledWaves.indexOf(waveType);\n return props.lineDistance[index] ?? 0.1;\n };\n\n const topLineCount = props.enabledWaves.includes('top') ? getLineCount('top') : 0;\n const middleLineCount = props.enabledWaves.includes('middle') ? getLineCount('middle') : 0;\n const bottomLineCount = props.enabledWaves.includes('bottom') ? getLineCount('bottom') : 0;\n\n const topLineDistance = props.enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01;\n const middleLineDistance = props.enabledWaves.includes('middle') ? getLineDistance('middle') * 0.01 : 0.01;\n const bottomLineDistance = props.enabledWaves.includes('bottom') ? getLineDistance('bottom') * 0.01 : 0.01;\n\n const scene = new Scene();\n\n const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n camera.position.z = 1;\n\n const renderer = new WebGLRenderer({ antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n containerRef.value.appendChild(renderer.domElement);\n\n const uniforms = {\n iTime: { value: 0 },\n iResolution: { value: new Vector3(1, 1, 1) },\n animationSpeed: { value: props.animationSpeed },\n\n enableTop: { value: props.enabledWaves.includes('top') },\n enableMiddle: { value: props.enabledWaves.includes('middle') },\n enableBottom: { value: props.enabledWaves.includes('bottom') },\n\n topLineCount: { value: topLineCount },\n middleLineCount: { value: middleLineCount },\n bottomLineCount: { value: bottomLineCount },\n\n topLineDistance: { value: topLineDistance },\n middleLineDistance: { value: middleLineDistance },\n bottomLineDistance: { value: bottomLineDistance },\n\n topWavePosition: {\n value: new Vector3(\n props.topWavePosition?.x ?? 10.0,\n props.topWavePosition?.y ?? 0.5,\n props.topWavePosition?.rotate ?? -0.4\n )\n },\n middleWavePosition: {\n value: new Vector3(\n props.middleWavePosition?.x ?? 5.0,\n props.middleWavePosition?.y ?? 0.0,\n props.middleWavePosition?.rotate ?? 0.2\n )\n },\n bottomWavePosition: {\n value: new Vector3(\n props.bottomWavePosition?.x ?? 2.0,\n props.bottomWavePosition?.y ?? -0.7,\n props.bottomWavePosition?.rotate ?? 0.4\n )\n },\n\n iMouse: { value: new Vector2(-1000, -1000) },\n interactive: { value: props.interactive },\n bendRadius: { value: props.bendRadius },\n bendStrength: { value: props.bendStrength },\n bendInfluence: { value: 0 },\n\n parallax: { value: props.parallax },\n parallaxStrength: { value: props.parallaxStrength },\n parallaxOffset: { value: new Vector2(0, 0) },\n\n lineGradient: {\n value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1))\n },\n lineGradientCount: { value: 0 }\n };\n\n if (props.linesGradient && props.linesGradient.length > 0) {\n const stops = props.linesGradient.slice(0, MAX_GRADIENT_STOPS);\n uniforms.lineGradientCount.value = stops.length;\n\n stops.forEach((hex, i) => {\n const color = hexToVec3(hex);\n uniforms.lineGradient.value[i].set(color.x, color.y, color.z);\n });\n }\n\n const material = new ShaderMaterial({\n uniforms,\n vertexShader,\n fragmentShader\n });\n\n const geometry = new PlaneGeometry(2, 2);\n const mesh = new Mesh(geometry, material);\n scene.add(mesh);\n\n const clock = new Clock();\n\n const setSize = () => {\n const el = containerRef.value!;\n const width = el.clientWidth || 1;\n const height = el.clientHeight || 1;\n\n renderer.setSize(width, height, false);\n\n const canvasWidth = renderer.domElement.width;\n const canvasHeight = renderer.domElement.height;\n uniforms.iResolution.value.set(canvasWidth, canvasHeight, 1);\n };\n\n setSize();\n\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(setSize) : null;\n\n if (ro && containerRef.value) {\n ro.observe(containerRef.value);\n }\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = renderer.domElement.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const dpr = renderer.getPixelRatio();\n\n targetMouseRef.value.set(x * dpr, (rect.height - y) * dpr);\n targetInfluenceRef.value = 1.0;\n\n if (props.parallax) {\n const centerX = rect.width / 2;\n const centerY = rect.height / 2;\n const offsetX = (x - centerX) / rect.width;\n const offsetY = -(y - centerY) / rect.height;\n targetParallaxRef.value.set(offsetX * props.parallaxStrength, offsetY * props.parallaxStrength);\n }\n };\n\n const handlePointerLeave = () => {\n targetInfluenceRef.value = 0.0;\n };\n\n if (props.interactive) {\n renderer.domElement.addEventListener('pointermove', handlePointerMove);\n renderer.domElement.addEventListener('pointerleave', handlePointerLeave);\n }\n\n let raf = 0;\n const renderLoop = () => {\n uniforms.iTime.value = clock.getElapsedTime();\n\n if (props.interactive) {\n currentMouseRef.value.lerp(targetMouseRef.value, props.mouseDamping);\n uniforms.iMouse.value.copy(currentMouseRef.value);\n\n currentInfluenceRef.value += (targetInfluenceRef.value - currentInfluenceRef.value) * props.mouseDamping;\n uniforms.bendInfluence.value = currentInfluenceRef.value;\n }\n\n if (props.parallax) {\n currentParallaxRef.value.lerp(targetParallaxRef.value, props.mouseDamping);\n uniforms.parallaxOffset.value.copy(currentParallaxRef.value);\n }\n\n renderer.render(scene, camera);\n raf = requestAnimationFrame(renderLoop);\n };\n renderLoop();\n\n cleanup = () => {\n cancelAnimationFrame(raf);\n if (ro && containerRef.value) {\n ro.disconnect();\n }\n\n if (props.interactive) {\n renderer.domElement.removeEventListener('pointermove', handlePointerMove);\n renderer.domElement.removeEventListener('pointerleave', handlePointerLeave);\n }\n\n geometry.dispose();\n material.dispose();\n renderer.dispose();\n if (renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n };\n};\n\nonMounted(() => {\n setup();\n});\n\nonBeforeUnmount(() => {\n cleanup?.();\n});\n\nwatch(\n () => [\n props.linesGradient,\n props.enabledWaves,\n props.lineCount,\n props.lineDistance,\n props.topWavePosition,\n props.middleWavePosition,\n props.bottomWavePosition,\n props.animationSpeed,\n props.interactive,\n props.bendRadius,\n props.bendStrength,\n props.mouseDamping,\n props.parallax,\n props.parallaxStrength\n ],\n () => {\n cleanup?.();\n setup();\n },\n {\n deep: true\n }\n);\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n class=\"relative w-full h-full overflow-hidden floating-lines-container\"\n :style=\"{\n mixBlendMode: mixBlendMode\n }\"\n />\n</template>\n","path":"FloatingLines/FloatingLines.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"three","version":"^0.178.0"}],"devDependencies":[],"categories":["Backgrounds"]} |