mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
1 line
10 KiB
JSON
1 line
10 KiB
JSON
{"name":"Ribbons","title":"Ribbons","description":"Flowing responsive ribbons/cursor trail driven by physics and pointer motion.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"ribbonsContainer\" class=\"relative w-full h-full overflow-hidden\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, watch, useTemplateRef } from 'vue';\nimport { Renderer, Transform, Vec3, Color, Polyline } from 'ogl';\n\ninterface RibbonsProps {\n colors?: string[];\n baseSpring?: number;\n baseFriction?: number;\n baseThickness?: number;\n offsetFactor?: number;\n maxAge?: number;\n pointCount?: number;\n speedMultiplier?: number;\n enableFade?: boolean;\n enableShaderEffect?: boolean;\n effectAmplitude?: number;\n backgroundColor?: number[];\n}\n\nconst props = withDefaults(defineProps<RibbonsProps>(), {\n colors: () => ['#ff9346', '#7cff67', '#ffee51', '#27FF64'],\n baseSpring: 0.03,\n baseFriction: 0.9,\n baseThickness: 30,\n offsetFactor: 0.05,\n maxAge: 500,\n pointCount: 50,\n speedMultiplier: 0.6,\n enableFade: false,\n enableShaderEffect: false,\n effectAmplitude: 2,\n backgroundColor: () => [0, 0, 0, 0]\n});\n\nconst ribbonsContainer = useTemplateRef<HTMLDivElement>('ribbonsContainer');\n\nlet renderer: Renderer;\nlet scene: Transform;\nlet lines: {\n spring: number;\n friction: number;\n mouseVelocity: Vec3;\n mouseOffset: Vec3;\n points: Vec3[];\n polyline: Polyline;\n}[] = [];\nlet frameId: number;\nlet lastTime = performance.now();\nconst mouse = new Vec3();\nlet resizeObserver: ResizeObserver | null = null;\n\nconst vertex = `\n precision highp float;\n \n attribute vec3 position;\n attribute vec3 next;\n attribute vec3 prev;\n attribute vec2 uv;\n attribute float side;\n \n uniform vec2 uResolution;\n uniform float uDPR;\n uniform float uThickness;\n uniform float uTime;\n uniform float uEnableShaderEffect;\n uniform float uEffectAmplitude;\n \n varying vec2 vUV;\n \n vec4 getPosition() {\n vec4 current = vec4(position, 1.0);\n vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);\n vec2 nextScreen = next.xy * aspect;\n vec2 prevScreen = prev.xy * aspect;\n vec2 tangent = normalize(nextScreen - prevScreen);\n vec2 normal = vec2(-tangent.y, tangent.x);\n normal /= aspect;\n normal *= mix(1.0, 0.1, pow(abs(uv.y - 0.5) * 2.0, 2.0));\n float dist = length(nextScreen - prevScreen);\n normal *= smoothstep(0.0, 0.02, dist);\n float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);\n float pixelWidth = current.w * pixelWidthRatio;\n normal *= pixelWidth * uThickness;\n current.xy -= normal * side;\n if(uEnableShaderEffect > 0.5) {\n current.xy += normal * sin(uTime + current.x * 10.0) * uEffectAmplitude;\n }\n return current;\n }\n \n void main() {\n vUV = uv;\n gl_Position = getPosition();\n }\n`;\n\nconst fragment = `\n precision highp float;\n uniform vec3 uColor;\n uniform float uOpacity;\n uniform float uEnableFade;\n varying vec2 vUV;\n void main() {\n float fadeFactor = 1.0;\n if(uEnableFade > 0.5) {\n fadeFactor = 1.0 - smoothstep(0.0, 1.0, vUV.y);\n }\n gl_FragColor = vec4(uColor, uOpacity * fadeFactor);\n }\n`;\n\nconst updateMouse = (e: MouseEvent | TouchEvent) => {\n const container = ribbonsContainer.value;\n if (!container) return;\n\n let x: number, y: number;\n const rect = container.getBoundingClientRect();\n\n if ('changedTouches' in e && e.changedTouches.length) {\n x = e.changedTouches[0].clientX - rect.left;\n y = e.changedTouches[0].clientY - rect.top;\n } else if (e instanceof MouseEvent) {\n x = e.clientX - rect.left;\n y = e.clientY - rect.top;\n } else {\n x = 0;\n y = 0;\n }\n\n const width = container.clientWidth;\n const height = container.clientHeight;\n mouse.set((x / width) * 2 - 1, (y / height) * -2 + 1, 0);\n};\n\nconst resize = () => {\n const container = ribbonsContainer.value;\n if (!container || !renderer) return;\n\n const width = container.clientWidth;\n const height = container.clientHeight;\n renderer.setSize(width, height);\n lines.forEach(line => line.polyline.resize());\n};\n\nconst createLines = () => {\n const center = (props.colors.length - 1) / 2;\n lines = [];\n\n props.colors.forEach((color, index) => {\n const spring = props.baseSpring + (Math.random() - 0.5) * 0.05;\n const friction = props.baseFriction + (Math.random() - 0.5) * 0.05;\n const thickness = props.baseThickness + (Math.random() - 0.5) * 3;\n const mouseOffset = new Vec3(\n (index - center) * props.offsetFactor + (Math.random() - 0.5) * 0.01,\n (Math.random() - 0.5) * 0.1,\n 0\n );\n\n const line = {\n spring,\n friction,\n mouseVelocity: new Vec3(),\n mouseOffset,\n points: [] as Vec3[],\n polyline: {} as Polyline\n };\n\n const count = props.pointCount;\n const points: Vec3[] = [];\n for (let i = 0; i < count; i++) {\n points.push(new Vec3());\n }\n line.points = points;\n\n line.polyline = new Polyline(renderer.gl, {\n points,\n vertex,\n fragment,\n uniforms: {\n uColor: { value: new Color(color) },\n uThickness: { value: thickness },\n uOpacity: { value: 1.0 },\n uTime: { value: 0.0 },\n uEnableShaderEffect: { value: props.enableShaderEffect ? 1.0 : 0.0 },\n uEffectAmplitude: { value: props.effectAmplitude },\n uEnableFade: { value: props.enableFade ? 1.0 : 0.0 }\n }\n });\n line.polyline.mesh.setParent(scene);\n lines.push(line);\n });\n};\n\nconst update = () => {\n frameId = requestAnimationFrame(update);\n const currentTime = performance.now();\n const dt = currentTime - lastTime;\n lastTime = currentTime;\n\n const tmp = new Vec3();\n lines.forEach(line => {\n tmp.copy(mouse).add(line.mouseOffset).sub(line.points[0]).multiply(line.spring);\n line.mouseVelocity.add(tmp).multiply(line.friction);\n line.points[0].add(line.mouseVelocity);\n\n for (let i = 1; i < line.points.length; i++) {\n if (isFinite(props.maxAge) && props.maxAge > 0) {\n const segmentDelay = props.maxAge / (line.points.length - 1);\n const alpha = Math.min(1, (dt * props.speedMultiplier) / segmentDelay);\n line.points[i].lerp(line.points[i - 1], alpha);\n } else {\n line.points[i].lerp(line.points[i - 1], 0.9);\n }\n }\n if (line.polyline.mesh.program.uniforms.uTime) {\n line.polyline.mesh.program.uniforms.uTime.value = currentTime * 0.001;\n }\n line.polyline.updateGeometry();\n });\n\n renderer.render({ scene });\n};\n\nconst initRibbons = () => {\n const container = ribbonsContainer.value;\n if (!container) return;\n\n renderer = new Renderer({ dpr: window.devicePixelRatio || 2, alpha: true });\n const gl = renderer.gl;\n\n if (Array.isArray(props.backgroundColor) && props.backgroundColor.length === 4) {\n gl.clearColor(\n props.backgroundColor[0],\n props.backgroundColor[1],\n props.backgroundColor[2],\n props.backgroundColor[3]\n );\n } else {\n gl.clearColor(0, 0, 0, 0);\n }\n\n gl.canvas.style.position = 'absolute';\n gl.canvas.style.top = '0';\n gl.canvas.style.left = '0';\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n container.appendChild(gl.canvas);\n\n scene = new Transform();\n\n createLines();\n\n container.addEventListener('mousemove', updateMouse);\n container.addEventListener('touchstart', updateMouse);\n container.addEventListener('touchmove', updateMouse);\n\n resize();\n\n if (typeof ResizeObserver !== 'undefined') {\n resizeObserver = new ResizeObserver(resize);\n resizeObserver.observe(container);\n } else {\n window.addEventListener('resize', resize);\n }\n\n update();\n};\n\nconst cleanup = () => {\n if (frameId) {\n cancelAnimationFrame(frameId);\n }\n\n if (resizeObserver) {\n resizeObserver.disconnect();\n } else {\n window.removeEventListener('resize', resize);\n }\n\n const container = ribbonsContainer.value;\n if (container) {\n container.removeEventListener('mousemove', updateMouse);\n container.removeEventListener('touchstart', updateMouse);\n container.removeEventListener('touchmove', updateMouse);\n\n if (renderer && renderer.gl.canvas && renderer.gl.canvas.parentNode === container) {\n container.removeChild(renderer.gl.canvas);\n }\n }\n};\n\nconst recreateLines = () => {\n lines.forEach(line => {\n if (line.polyline.mesh && line.polyline.mesh.parent) {\n line.polyline.mesh.setParent(null);\n }\n });\n\n createLines();\n};\n\nwatch(\n () => [props.colors, props.pointCount],\n () => {\n if (renderer && scene) {\n recreateLines();\n }\n },\n { deep: true }\n);\n\nwatch(\n () => [props.baseThickness, props.enableFade, props.enableShaderEffect, props.effectAmplitude, props.backgroundColor],\n () => {\n if (renderer && lines.length > 0) {\n lines.forEach(line => {\n if (line.polyline.mesh.program.uniforms.uEnableFade) {\n line.polyline.mesh.program.uniforms.uEnableFade.value = props.enableFade ? 1.0 : 0.0;\n }\n if (line.polyline.mesh.program.uniforms.uEnableShaderEffect) {\n line.polyline.mesh.program.uniforms.uEnableShaderEffect.value = props.enableShaderEffect ? 1.0 : 0.0;\n }\n if (line.polyline.mesh.program.uniforms.uEffectAmplitude) {\n line.polyline.mesh.program.uniforms.uEffectAmplitude.value = props.effectAmplitude;\n }\n });\n\n const gl = renderer.gl;\n if (Array.isArray(props.backgroundColor) && props.backgroundColor.length === 4) {\n gl.clearColor(\n props.backgroundColor[0],\n props.backgroundColor[1],\n props.backgroundColor[2],\n props.backgroundColor[3]\n );\n } else {\n gl.clearColor(0, 0, 0, 0);\n }\n }\n },\n { deep: true }\n);\n\nonMounted(() => {\n initRibbons();\n});\n\nonUnmounted(() => {\n cleanup();\n});\n</script>\n","path":"Ribbons/Ribbons.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"ogl","version":"^1.0.11"}],"devDependencies":[],"categories":["Animations"]} |