mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
6.9 KiB
JSON
1 line
6.9 KiB
JSON
{"name":"GridDistortion","title":"GridDistortion","description":"Warped grid mesh distorts smoothly reacting to cursor.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport * as THREE from 'three';\nimport { onMounted, onUnmounted, ref, watch, useTemplateRef } from 'vue';\n\ninterface GridDistortionProps {\n grid?: number;\n mouse?: number;\n strength?: number;\n relaxation?: number;\n imageSrc: string;\n className?: string;\n}\n\nconst props = withDefaults(defineProps<GridDistortionProps>(), {\n grid: 15,\n mouse: 0.1,\n strength: 0.15,\n relaxation: 0.9,\n className: ''\n});\n\nconst vertexShader = `\nuniform float time;\nvarying vec2 vUv;\nvarying vec3 vPosition;\n\nvoid main() {\n vUv = uv;\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform sampler2D uDataTexture;\nuniform sampler2D uTexture;\nuniform vec4 resolution;\nvarying vec2 vUv;\n\nvoid main() {\n vec2 uv = vUv;\n vec4 offset = texture2D(uDataTexture, vUv);\n gl_FragColor = texture2D(uTexture, uv - 0.02 * offset.rg);\n}\n`;\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nconst imageAspectRef = ref(1);\nconst cameraRef = ref<THREE.OrthographicCamera | null>(null);\nconst initialDataRef = ref<Float32Array | null>(null);\n\nlet cleanupAnimation: () => void = () => {};\n\nconst setupAnimation = () => {\n const container = containerRef.value;\n if (!container) return;\n\n const scene = new THREE.Scene();\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n powerPreference: 'high-performance'\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n container.appendChild(renderer.domElement);\n\n const camera = new THREE.OrthographicCamera(0, 0, 0, 0, -1000, 1000);\n camera.position.z = 2;\n cameraRef.value = camera;\n\n const uniforms = {\n time: { value: 0 },\n resolution: { value: new THREE.Vector4() },\n uTexture: { value: null as THREE.Texture | null },\n uDataTexture: { value: null as THREE.DataTexture | null }\n };\n\n const textureLoader = new THREE.TextureLoader();\n textureLoader.load(props.imageSrc, texture => {\n texture.minFilter = THREE.LinearFilter;\n imageAspectRef.value = texture.image.width / texture.image.height;\n uniforms.uTexture.value = texture;\n handleResize();\n });\n\n const size = props.grid;\n const data = new Float32Array(4 * size * size);\n for (let i = 0; i < size * size; i++) {\n data[i * 4] = Math.random() * 255 - 125;\n data[i * 4 + 1] = Math.random() * 255 - 125;\n }\n initialDataRef.value = new Float32Array(data);\n\n const dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);\n dataTexture.needsUpdate = true;\n uniforms.uDataTexture.value = dataTexture;\n\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n uniforms,\n vertexShader,\n fragmentShader\n });\n const geometry = new THREE.PlaneGeometry(1, 1, size - 1, size - 1);\n const plane = new THREE.Mesh(geometry, material);\n scene.add(plane);\n\n const handleResize = () => {\n const width = container.offsetWidth;\n const height = container.offsetHeight;\n const containerAspect = width / height;\n const imageAspect = imageAspectRef.value;\n\n renderer.setSize(width, height);\n\n const scale = Math.max(containerAspect / imageAspect, 1);\n plane.scale.set(imageAspect * scale, scale, 1);\n\n const frustumHeight = 1;\n const frustumWidth = frustumHeight * containerAspect;\n camera.left = -frustumWidth / 2;\n camera.right = frustumWidth / 2;\n camera.top = frustumHeight / 2;\n camera.bottom = -frustumHeight / 2;\n camera.updateProjectionMatrix();\n\n uniforms.resolution.value.set(width, height, 1, 1);\n };\n\n const mouseState = {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n };\n\n const handleMouseMove = (e: MouseEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = 1 - (e.clientY - rect.top) / rect.height;\n mouseState.vX = x - mouseState.prevX;\n mouseState.vY = y - mouseState.prevY;\n Object.assign(mouseState, { x, y, prevX: x, prevY: y });\n };\n\n const handleMouseLeave = () => {\n dataTexture.needsUpdate = true;\n Object.assign(mouseState, {\n x: 0,\n y: 0,\n prevX: 0,\n prevY: 0,\n vX: 0,\n vY: 0\n });\n };\n\n container.addEventListener('mousemove', handleMouseMove);\n container.addEventListener('mouseleave', handleMouseLeave);\n window.addEventListener('resize', handleResize);\n handleResize();\n\n const animate = () => {\n requestAnimationFrame(animate);\n uniforms.time.value += 0.05;\n\n const data = dataTexture.image.data as Float32Array;\n for (let i = 0; i < size * size; i++) {\n data[i * 4] *= props.relaxation;\n data[i * 4 + 1] *= props.relaxation;\n }\n\n const gridMouseX = size * mouseState.x;\n const gridMouseY = size * mouseState.y;\n const maxDist = size * props.mouse;\n\n for (let i = 0; i < size; i++) {\n for (let j = 0; j < size; j++) {\n const distSq = Math.pow(gridMouseX - i, 2) + Math.pow(gridMouseY - j, 2);\n if (distSq < maxDist * maxDist) {\n const index = 4 * (i + size * j);\n const power = Math.min(maxDist / Math.sqrt(distSq), 10);\n data[index] += props.strength * 100 * mouseState.vX * power;\n data[index + 1] -= props.strength * 100 * mouseState.vY * power;\n }\n }\n }\n\n dataTexture.needsUpdate = true;\n renderer.render(scene, camera);\n };\n animate();\n\n cleanupAnimation = () => {\n container.removeEventListener('mousemove', handleMouseMove);\n container.removeEventListener('mouseleave', handleMouseLeave);\n window.removeEventListener('resize', handleResize);\n renderer.dispose();\n geometry.dispose();\n material.dispose();\n dataTexture.dispose();\n if (uniforms.uTexture.value) uniforms.uTexture.value.dispose();\n };\n};\n\nonMounted(() => {\n cleanupAnimation();\n setupAnimation();\n});\n\nonUnmounted(() => {\n const container = containerRef.value;\n if (container) {\n container.innerHTML = '';\n }\n cleanupAnimation();\n});\n\nwatch(\n () => props,\n () => {\n cleanupAnimation();\n if (containerRef.value) {\n setupAnimation();\n }\n },\n { immediate: true }\n);\n</script>\n\n<template>\n <div ref=\"containerRef\" :class=\"[props.className, 'w-full h-full overflow-hidden']\" />\n</template>\n","path":"GridDistortion/GridDistortion.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"three","version":"^0.178.0"}],"devDependencies":[],"categories":["Backgrounds"]} |