{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "ripple-distortion", "type": "registry:ui", "title": "Ripple Distortion", "description": "An interactive image distortion effect with mouse-driven ripples using Three.js.", "dependencies": [ "three" ], "files": [ { "path": "registry/ruixenui/ripple-distortion.tsx", "content": "\"use client\";\n\nimport React, { useRef, useEffect } from \"react\";\nimport * as THREE from \"three\";\n\ninterface RippleDistortionProps {\n imageSrc: string;\n width?: string | number;\n height?: string | number;\n frequency?: number;\n amplitude?: number;\n speed?: number;\n className?: string;\n}\n\nconst vertexShader = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform float time;\nuniform sampler2D uTexture;\nuniform vec2 uMouse;\nuniform float frequency;\nuniform float amplitude;\nuniform float speed;\nuniform vec2 uScale;\n\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n\n // maintain cover aspect ratio\n uv = (uv - 0.5) * uScale + 0.5;\n\n float dist = distance(uv, uMouse);\n float ripple = sin(dist * frequency - time * speed) * amplitude / (dist + 0.1);\n vec2 distortedUv = uv + normalize(uv - uMouse) * ripple;\n\n gl_FragColor = texture2D(uTexture, distortedUv);\n}\n`;\n\nconst RippleDistortion: React.FC = ({\n imageSrc,\n width = \"100%\",\n height = \"100%\",\n frequency = 30.0,\n amplitude = 0.02,\n speed = 5.0,\n className = \"\",\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const animationIdRef = useRef(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const scene = new THREE.Scene();\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x000000, 0);\n renderer.domElement.style.width = \"100%\";\n renderer.domElement.style.height = \"100%\";\n renderer.domElement.style.display = \"block\";\n container.innerHTML = \"\";\n container.appendChild(renderer.domElement);\n rendererRef.current = renderer;\n\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);\n camera.position.z = 1;\n\n const uniforms = {\n time: { value: 0 },\n uTexture: { value: null as THREE.Texture | null },\n uMouse: { value: new THREE.Vector2(0.5, 0.5) },\n frequency: { value: frequency },\n amplitude: { value: amplitude },\n speed: { value: speed },\n uScale: { value: new THREE.Vector2(1, 1) },\n };\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n const material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms,\n transparent: true,\n });\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n const loader = new THREE.TextureLoader();\n loader.load(imageSrc, (texture) => {\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.wrapS = THREE.ClampToEdgeWrapping;\n texture.wrapT = THREE.ClampToEdgeWrapping;\n uniforms.uTexture.value = texture;\n resize();\n });\n\n const resize = () => {\n const rect = container.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n if (w === 0 || h === 0) return;\n\n renderer.setSize(w, h, false);\n\n const texture = uniforms.uTexture.value;\n if (texture && texture.image) {\n const imageAspect = texture.image.width / texture.image.height;\n const screenAspect = w / h;\n\n if (imageAspect > screenAspect) {\n uniforms.uScale.value.set(screenAspect / imageAspect, 1.0);\n } else {\n uniforms.uScale.value.set(1.0, imageAspect / screenAspect);\n }\n }\n };\n\n window.addEventListener(\"resize\", resize);\n resize();\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1.0 - (e.clientY - rect.top) / rect.height;\n uniforms.uMouse.value.set(x, y);\n };\n\n container.addEventListener(\"mousemove\", handleMouseMove);\n\n const animate = () => {\n animationIdRef.current = requestAnimationFrame(animate);\n uniforms.time.value += 0.02;\n renderer.render(scene, camera);\n };\n\n animate();\n\n return () => {\n if (animationIdRef.current) cancelAnimationFrame(animationIdRef.current);\n window.removeEventListener(\"resize\", resize);\n container.removeEventListener(\"mousemove\", handleMouseMove);\n renderer.dispose();\n geometry.dispose();\n material.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n };\n }, [imageSrc, frequency, amplitude, speed]);\n\n return (\n \n );\n};\n\nexport default RippleDistortion;\n", "type": "registry:ui", "target": "components/ruixen/ripple-distortion.tsx" } ] }